Compare commits

...

84 Commits

Author SHA1 Message Date
David Peter
6a3101b0be [ty] Improve specialization-error diagnostics 2025-09-10 11:44:33 +02:00
David Peter
0f371f6efd Merge remote-tracking branch 'origin/main' into david/signature-implicit-self 2025-09-10 11:12:03 +02:00
David Peter
cbaac56cf1 [ty] Use 'unknown' specialization for upper bound on Self 2025-09-10 11:04:54 +02:00
Brent Westbrook
9cb37db510 Bump LibCST to 1.8.4 (#20321)
This should fix the fuzz build on `main`. They added support for
t-strings, which made one of our matches non-exhaustive.

https://github.com/Instagram/LibCST/releases/tag/v1.8.4
2025-09-09 17:34:33 -04:00
Douglas Creager
ed06fb5ce2 [ty] Use partial-order-friendly representation of typevar constraints (#20306)
The constraint representation that we added in #19997 was subtly wrong,
in that it didn't correctly model that type assignability is a _partial_
order — it's possible for two types to be incomparable, with neither a
subtype of the other. That means the negation of a constraint like `T ≤
t` (typevar `T` must be a subtype of `t`) is **_not_** `t < T`, but
rather `t < T ∨ T ≁ t` (using ≁ to mean "not comparable to").

That means we need to update our constraint representation to be an
enum, so that we can track both _range_ constraints (upper/lower bound
on the typevar), and these new _incomparable_ constraints.

Since we need an enum now, that also lets us simplify how we were
modeling range constraints. Before, we let the lower/upper bounds be
either open (<) or closed (≤). Now, range constraints are always closed,
and we add a third kind of constraint for _not equivalent_ (≠). We can
translate an open upper bound `T < t` into `T ≤ t ∧ T ≠ t`.

We already had the logic for doing adding _clauses_ to a _set_ by doing
a pairwise simplification. We copy that over to where we add
_constraints_ to a _clause_. To calculate the intersection or union of
two constraints, the new enum representation makes it easy to break down
all of the possibilities into a small number of cases: intersect range
with range, intersect range with not-equivalent, etc. I've done the math
[here](https://dcreager.net/theory/constraints/) to show that the
simplifications for each of these cases is correct.
2025-09-09 15:54:47 -04:00
Igor Drokin
54df73c9f7 [pyupgrade] Apply UP008 only when the __class__ cell exists (#19424)
## Summary

Resolves #19357 

Skip UP008 diagnostic for `builtins.super(P, self)` calls when
`__class__` is not referenced locally, preventing incorrect fixes.

**Note:** I haven't found concrete information about which cases
`__class__` will be loaded into the scope. Let me know if anyone has
references, it would be useful to enhance the implementation. I did a
lot of tests to determine when `__class__` is loaded. Considered
sources:
1. [Python doc
super](https://docs.python.org/3/library/functions.html#super)
2. [Python doc classes](https://docs.python.org/3/tutorial/classes.html)
3. [pep-3135](https://peps.python.org/pep-3135/#specification)

As I understand it, Python will inject at runtime into local scope a
`__class__` variable if it detects references to `super` or `__class__`.
This allows calling `super()` and passing appropriate parameters.
However, the compiler doesn't do the same for `builtins.super`, so we
need to somehow introduce `__class__` into the local scope.

I figured out `__class__` will be in scope with valid value when two
conditions are met:
1. `super` or `__class__` names have been loaded within function scope
4. `__class__` is not overridden.

I think my solution isn't elegant, so I would be appreciate a detailed
review.

## Test Plan

Added 19 test cases, updated snapshots.

---------

Co-authored-by: Igor Drokin <drokinii1017@gmail.com>
2025-09-09 14:59:23 -04:00
Amethyst Reese
d7524ea6d4 Refactor diagnostic start|end location helpers (#20309)
- Renames functions to drop `expect_` from names.
- Make functions return `Option<LineColumn>` to appropriately signal
  when range is not available.
- Update existing consumers to use `unwrap_or_default()`. Uncertain if
  there are better fallback behaviors for individual consumers.
2025-09-09 11:39:31 -07:00
Alex Waygood
bf66178959 [ty] Add tests for protocols with generic method members (#20316) 2025-09-09 16:44:00 +00:00
Zanie Blue
9cdac2d6fb Add support for using uv as an alternative formatter backend (#19665)
This adds a new `backend: internal | uv` option to the LSP
`FormatOptions` allowing users to perform document and range formatting
operations though uv. The idea here is to prototype a solution for users
to transition to a `uv format` command without encountering version
mismatches (and consequently, formatting differences) between the LSP's
version of `ruff` and uv's version of `ruff`.

The primarily alternative to this would be to use uv to discover the
`ruff` version used to start the LSP in the first place. However, this
would increase the scope of a minimal `uv format` command beyond "run a
formatter", and raise larger questions about how uv should be used to
coordinate toolchain discovery. I think those are good things to
explore, but I'm hesitant to let them block a `uv format`
implementation. Another downside of using uv to discover `ruff`, is that
it needs to be implemented _outside_ the LSP; e.g., we'd need to change
the instructions on how to run the LSP and implement it in each editor
integration, like the VS Code plugin.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-09-09 20:39:53 +05:30
Igor Drokin
79706a2e26 [pyupgrade] Enable rule triggering for stub files (UP043) (#20027)
## Summary
Resolves #20011

Implemented alternative triggering condition for rule
[`UP043`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/)
based on requirements outlined in [issue
#20011](https://github.com/astral-sh/ruff/issues/20011)
## Test Plan
Created .pyi file to ensure triggering the rule

---------

Co-authored-by: Igor Drokin <drokinii1017@gmail.com>
Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-09-09 12:57:26 +00:00
Andrew Gallant
25853e2377 Allow the if_not_else Clippy lint
Specifically, the [`if_not_else`] lint will sometimes flag
code to change the order of `if` and `else` bodies if this
would allow a `!` to be removed. While perhaps tasteful in
some cases, there are many cases in my experience where this
bows to other competing concerns that impact readability.
(Such as the relative sizes of the `if` and `else` bodies,
or perhaps an ordering that just makes the code flow in a
more natural way.)

[`if_not_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#/if_not_else
2025-09-09 08:49:25 -04:00
David Peter
99ecb657f6 Minor fixes 2025-09-09 08:47:14 +02:00
Renkai Ge
61f906d8e7 [ty] equality narrowing on enums that don't override __eq__ or __ne__ (#20285)
Add equality narrowing for enums, if they don't override `__eq__` or `__ne__` in an unsafe way.

Follow-up to PR https://github.com/astral-sh/ruff/pull/20164

Fixes https://github.com/astral-sh/ty/issues/939
2025-09-08 16:56:28 -07:00
Shunsuke Shibayama
08a561fc05 [ty] more precise lazy scope place lookup (#19932)
## Summary

This is a follow-up to https://github.com/astral-sh/ruff/pull/19321.

Now lazy snapshots are updated to take into account new bindings on
every symbol reassignment.

```python
def outer(x: A | None):
    if x is None:
        x = A()

    reveal_type(x)  # revealed: A

    def inner() -> None:
        # lazy snapshot: {x: A}
        reveal_type(x)  # revealed: A
    inner()

def outer() -> None:
    x = None

    x = 1

    def inner() -> None:
        # lazy snapshot: {x: Literal[1]} -> {x: Literal[1, 2]}
        reveal_type(x)  # revealed: Literal[1, 2]
    inner()

    x = 2
```

Closes astral-sh/ty#559.

## Test Plan

Some TODOs in `public_types.md` now work properly.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-09-08 21:08:35 +00:00
Ibraheem Ahmed
aa5d665d52 [ty] Add support for generic PEP695 type aliases (#20219)
## Summary

Adds support for generic PEP695 type aliases, e.g.,
```python
type A[T] = T
reveal_type(A[int]) # A[int]
```

Resolves https://github.com/astral-sh/ty/issues/677.
2025-09-08 13:26:21 -07:00
David Peter
d55edb3d74 [ty] Support "legacy" typing.Self in combination with PEP 695 generic contexts (#20304)
## Summary

Support cases like the following, where we need the generic context to
include both `Self` and `T` (not just `T`):

```py
from typing import Self

class C:
    def method[T](self: Self, arg: T): ...

C().method(1)
```

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

## Test Plan

Added regression test
2025-09-08 16:57:09 +02:00
arielle
ab86ae1760 [pep8-naming] Fix formatting of __all__ (N816) (#20301)
<!--
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

Noticed this was not escaped when writing a project that parses the
result of `ruff rule --outputformat json`. This is visible here:
<https://docs.astral.sh/ruff/rules/mixed-case-variable-in-global-scope/#why-is-this-bad>

## Test Plan

documentation only

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-09-08 14:40:38 +00:00
David Peter
31566d67cb Update tests 2025-09-08 15:49:40 +02:00
David Peter
f0b0d2ef87 [ty] Merge legacy and PEP 695 generic contexts 2025-09-08 15:42:21 +02:00
David Peter
916968d0ff [ty] Fix signature of NamedTupleLike._make (#20302) 2025-09-08 14:53:17 +02:00
Alex Waygood
deb3d3d150 [ty] Fall back to object for attribute access on synthesized protocols (#20286) 2025-09-08 13:04:37 +01:00
David Peter
4c7f7d199b Update diagnostic count 2025-09-08 13:30:56 +02:00
David Peter
a66add41be Make Self an inferrable TypeVar 2025-09-08 13:08:16 +02:00
Frank Dana
982a0a2a7c CI: Eliminate warning in fuzz build workflow (#20290)
## Summary

Pr #11919 changed the fuzz build from `taiki-e/install-action` to
`cargo-bins/cargo-binstall` for necessary reasons of version selection.
But it left the `with:` parameter, which the `binstall` action does not
support. As a result, all workflow runs are showing a warning:
> Unexpected input(s) `'tool'`, valid inputs are `['']`

Eliminate the warning by removing the `with` parameter.

## Test Plan

Run CI, determine that the "cargo fuzz build" step no longer includes an
Annotation showing the warning message (quoted above).
2025-09-08 11:33:38 +05:30
renovate[bot]
f893b19930 Update dependency ruff to v0.12.12 (#20293)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [ruff](https://docs.astral.sh/ruff)
([source](https://redirect.github.com/astral-sh/ruff),
[changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md))
| `==0.12.11` -> `==0.12.12` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.12.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.12.11/0.12.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

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

---

### Release Notes

<details>
<summary>astral-sh/ruff (ruff)</summary>

###
[`v0.12.12`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#01212)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.12.11...0.12.12)

##### Preview features

- Show fixes by default
([#&#8203;19919](https://redirect.github.com/astral-sh/ruff/pull/19919))
- \[`airflow`] Convert `DatasetOrTimeSchedule(datasets=...)` to
`AssetOrTimeSchedule(assets=...)` (`AIR311`)
([#&#8203;20202](https://redirect.github.com/astral-sh/ruff/pull/20202))
- \[`airflow`] Improve the `AIR002` error message
([#&#8203;20173](https://redirect.github.com/astral-sh/ruff/pull/20173))
- \[`airflow`] Move `airflow.operators.postgres_operator.Mapping` from
`AIR302` to `AIR301`
([#&#8203;20172](https://redirect.github.com/astral-sh/ruff/pull/20172))
- \[`flake8-async`] Implement `blocking-input` rule (`ASYNC250`)
([#&#8203;20122](https://redirect.github.com/astral-sh/ruff/pull/20122))
- \[`flake8-use-pathlib`] Make `PTH119` and `PTH120` fixes unsafe
because they can change behavior
([#&#8203;20118](https://redirect.github.com/astral-sh/ruff/pull/20118))
- \[`pylint`] Add U+061C to `PLE2502`
([#&#8203;20106](https://redirect.github.com/astral-sh/ruff/pull/20106))
- \[`ruff`] Fix false negative for empty f-strings in `deque` calls
(`RUF037`)
([#&#8203;20109](https://redirect.github.com/astral-sh/ruff/pull/20109))

##### Bug fixes

- Less confidently mark f-strings as empty when inferring truthiness
([#&#8203;20152](https://redirect.github.com/astral-sh/ruff/pull/20152))
- \[`fastapi`] Fix false positive for paths with spaces around
parameters (`FAST003`)
([#&#8203;20077](https://redirect.github.com/astral-sh/ruff/pull/20077))
- \[`flake8-comprehensions`] Skip `C417` when lambda contains
`yield`/`yield from`
([#&#8203;20201](https://redirect.github.com/astral-sh/ruff/pull/20201))
- \[`perflint`] Handle tuples in dictionary comprehensions (`PERF403`)
([#&#8203;19934](https://redirect.github.com/astral-sh/ruff/pull/19934))

##### Rule changes

- \[`pycodestyle`] Preserve return type annotation for `ParamSpec`
(`E731`)
([#&#8203;20108](https://redirect.github.com/astral-sh/ruff/pull/20108))

##### Documentation

- Add fix safety sections to docs
([#&#8203;17490](https://redirect.github.com/astral-sh/ruff/pull/17490),[#&#8203;17499](https://redirect.github.com/astral-sh/ruff/pull/17499))

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 11:03:19 +05:30
renovate[bot]
c96ebe3936 Update cargo-bins/cargo-binstall action to v1.15.4 (#20292)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cargo-bins/cargo-binstall](https://redirect.github.com/cargo-bins/cargo-binstall)
| action | patch | `v1.15.3` -> `v1.15.4` |

---

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

---

### Release Notes

<details>
<summary>cargo-bins/cargo-binstall (cargo-bins/cargo-binstall)</summary>

###
[`v1.15.4`](https://redirect.github.com/cargo-bins/cargo-binstall/releases/tag/v1.15.4)

[Compare
Source](https://redirect.github.com/cargo-bins/cargo-binstall/compare/v1.15.3...v1.15.4)

*Binstall is a tool to fetch and install Rust-based executables as
binaries. It aims to be a drop-in replacement for `cargo install` in
most cases. Install it today with `cargo install cargo-binstall`, from
the binaries below, or if you already have it, upgrade with `cargo
binstall cargo-binstall`.*

##### In this release:

- clarify `--install-path` behavior when installing from source
([#&#8203;2259](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2259)
[#&#8203;2294](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2294))

##### Other changes:

- bump dependencies
([#&#8203;2295](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2295)
[#&#8203;2298](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2298)
[#&#8203;2299](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2299))

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 11:03:07 +05:30
renovate[bot]
22ca5dd890 Update dependency mdformat-mkdocs to v4.4.1 (#20299)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[mdformat-mkdocs](https://redirect.github.com/kyleking/mdformat-mkdocs)
([changelog](https://redirect.github.com/kyleking/mdformat-mkdocs/releases))
| `==4.3.0` -> `==4.4.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/mdformat-mkdocs/4.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mdformat-mkdocs/4.3.0/4.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

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

---

### Release Notes

<details>
<summary>kyleking/mdformat-mkdocs (mdformat-mkdocs)</summary>

###
[`v4.4.1`](https://redirect.github.com/KyleKing/mdformat-mkdocs/releases/tag/v4.4.1)

[Compare
Source](https://redirect.github.com/kyleking/mdformat-mkdocs/compare/v4.4.0...v4.4.1)

##### What's Changed

-
fix([#&#8203;56](https://redirect.github.com/kyleking/mdformat-mkdocs/issues/56)):
narrowly scope escape\_deflist by
[@&#8203;KyleKing](https://redirect.github.com/KyleKing) in
[KyleKing#57](https://redirect.github.com/KyleKing/mdformat-mkdocs/pull/57)

**Full Changelog**:
<https://github.com/KyleKing/mdformat-mkdocs/compare/v4.4.0...v4.4.1>

###
[`v4.4.0`](https://redirect.github.com/KyleKing/mdformat-mkdocs/releases/tag/v4.4.0)

[Compare
Source](https://redirect.github.com/kyleking/mdformat-mkdocs/compare/v4.3.0...v4.4.0)

##### What's Changed

-
fix([#&#8203;54](https://redirect.github.com/kyleking/mdformat-mkdocs/issues/54)):
add 4-space indented deflists by
[@&#8203;KyleKing](https://redirect.github.com/KyleKing) in
[KyleKing#55](https://redirect.github.com/KyleKing/mdformat-mkdocs/pull/55)

**Full Changelog**:
<https://github.com/KyleKing/mdformat-mkdocs/compare/v4.3.0...v4.4.0>

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 11:02:55 +05:30
renovate[bot]
f7995f4aef Update astral-sh/setup-uv action to v6.6.1 (#20291)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | patch | `v6.6.0` -> `v6.6.1` |

---

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

---

### Release Notes

<details>
<summary>astral-sh/setup-uv (astral-sh/setup-uv)</summary>

###
[`v6.6.1`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.6.1):
🌈 Fix exclusions in cache-dependency-glob

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.6.0...v6.6.1)

##### Changes

Exclusions with a leading `!` in the
[cache-dependency-glob](https://redirect.github.com/astral-sh/setup-uv?tab=readme-ov-file#cache-dependency-glob)
did not work and got fixed with this release. Thank you
[@&#8203;KnisterPeter](https://redirect.github.com/KnisterPeter) for
raising this!

##### 🐛 Bug fixes

- Fix exclusions in cache-dependency-glob
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;546](https://redirect.github.com/astral-sh/setup-uv/issues/546))

##### 🧰 Maintenance

- Bump dependencies
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;547](https://redirect.github.com/astral-sh/setup-uv/issues/547))
- chore: update known versions for 0.8.14
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;543](https://redirect.github.com/astral-sh/setup-uv/issues/543))
- chore: update known versions for 0.8.13
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;536](https://redirect.github.com/astral-sh/setup-uv/issues/536))

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 11:00:16 +05:30
renovate[bot]
480fb278d0 Update Rust crate bitflags to v2.9.4 (#20294)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [bitflags](https://redirect.github.com/bitflags/bitflags) |
workspace.dependencies | patch | `2.9.3` -> `2.9.4` |

---

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

---

### Release Notes

<details>
<summary>bitflags/bitflags (bitflags)</summary>

###
[`v2.9.4`](https://redirect.github.com/bitflags/bitflags/blob/HEAD/CHANGELOG.md#294)

[Compare
Source](https://redirect.github.com/bitflags/bitflags/compare/2.9.3...2.9.4)

#### What's Changed

- Add Cargo features to readme by
[@&#8203;KodrAus](https://redirect.github.com/KodrAus) in
[#&#8203;460](https://redirect.github.com/bitflags/bitflags/pull/460)

**Full Changelog**:
<https://github.com/bitflags/bitflags/compare/2.9.3...2.9.4>

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 10:59:59 +05:30
renovate[bot]
b2f364d9cb Update Rust crate clap to v4.5.47 (#20295)
This PR contains the following updates:

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

---

> [!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.47`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4547---2025-09-02)

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

##### Features

- Added `impl FromArgMatches for ()`
- Added `impl Args for ()`
- Added `impl Subcommand for ()`
- Added `impl FromArgMatches for Infallible`
- Added `impl Subcommand for Infallible`

##### Fixes

- *(derive)* Update runtime error text to match `clap`

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 10:59:47 +05:30
renovate[bot]
aa82137d9f Update Rust crate wasm-bindgen-test to v0.3.51 (#20298)
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.50` -> `0.3.51` |

---

> [!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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 10:59:29 +05:30
renovate[bot]
adfe2438e9 Update Rust crate log to v0.4.28 (#20297)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [log](https://redirect.github.com/rust-lang/log) |
workspace.dependencies | patch | `0.4.27` -> `0.4.28` |

---

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

---

### Release Notes

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

###
[`v0.4.28`](https://redirect.github.com/rust-lang/log/blob/HEAD/CHANGELOG.md#0428---2025-09-02)

[Compare
Source](https://redirect.github.com/rust-lang/log/compare/0.4.27...0.4.28)

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 10:59:09 +05:30
renovate[bot]
3247991429 Update Rust crate insta to v1.43.2 (#20296)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [insta](https://insta.rs/)
([source](https://redirect.github.com/mitsuhiko/insta)) |
workspace.dependencies | patch | `1.43.1` -> `1.43.2` |

---

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

---

### Release Notes

<details>
<summary>mitsuhiko/insta (insta)</summary>

###
[`v1.43.2`](https://redirect.github.com/mitsuhiko/insta/blob/HEAD/CHANGELOG.md#1432)

[Compare
Source](https://redirect.github.com/mitsuhiko/insta/compare/1.43.1...1.43.2)

- Fix panics when `cargo metadata` fails to execute or parse (e.g., when
cargo is not in PATH or returns invalid output). Now falls back to using
the manifest directory as the workspace root.
[#&#8203;798](https://redirect.github.com/mitsuhiko/insta/issues/798)
([@&#8203;adriangb](https://redirect.github.com/adriangb))
- Fix clippy `uninlined_format_args` lint warnings.
[#&#8203;801](https://redirect.github.com/mitsuhiko/insta/issues/801)
- Changed diff line numbers to 1-based indexing.
[#&#8203;799](https://redirect.github.com/mitsuhiko/insta/issues/799)
- Preserve snapshot names with `INSTA_GLOB_FILTER`.
[#&#8203;786](https://redirect.github.com/mitsuhiko/insta/issues/786)
- Bumped `libc` crate to `0.2.174`, fixing building on musl targets, and
increasing the MSRV of
`insta` to `1.64.0` (released Sept 2022).
[#&#8203;784](https://redirect.github.com/mitsuhiko/insta/issues/784)
- Fix clippy 1.88 errors.
[#&#8203;783](https://redirect.github.com/mitsuhiko/insta/issues/783)
- Fix source path in snapshots for non-child workspaces.
[#&#8203;778](https://redirect.github.com/mitsuhiko/insta/issues/778)
- Add lifetime to Selector in redaction iterator.
[#&#8203;779](https://redirect.github.com/mitsuhiko/insta/issues/779)

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 10:58:52 +05:30
justin
08fcf7e106 [ty] initial support for slots=True in dataclasses (#20278) 2025-09-07 18:25:35 +01:00
Carl Meyer
2467c4352e [ty] propagate visitors and constraints through has_relation_in_invariant_position (#20259)
## Summary

The sub-checks for assignability and subtyping of materializations
performed in `has_relation_in_invariant_position` and
`is_subtype_in_invariant_position` need to propagate the
`HasRelationToVisitor`, or we can stack overflow.

A side effect of this change is that we also propagate the
`ConstraintSet` through, rather than using `C::from_bool`, which I think
may also become important for correctness in cases involving type
variables (though it isn't testable yet, since we aren't yet actually
creating constraints other than always-true and always-false.)

## Test Plan

Added mdtest (derived from code found in pydantic) which
stack-overflowed before this PR.

With this change incorporated, pydantic now checks successfully on my
draft PR for PEP 613 TypeAlias support.
2025-09-06 00:17:17 +00:00
Amethyst Reese
a27c64811e Add support for sorting diagnostics without a range (#20257)
- Update ruff ordering compare to work with optional ranges (treating
  them as starting from zero)
2025-09-05 15:23:30 -07:00
Glyphack
4064fa28fc Add more examples for self mdtest 2025-09-06 00:08:23 +02:00
Glyphack
a51982bac6 Update mdtests 2025-09-05 20:59:21 +02:00
Shaygan Hooshyari
a27013bdd3 Merge branch 'main' into typing-self-argument 2025-09-05 20:42:39 +02:00
Alex Waygood
5d52902e18 [ty] Implement the legacy PEP-484 convention for indicating positional-only parameters (#20248)
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-09-05 17:56:06 +01:00
Glyphack
c24236cc73 Update mdtests 2025-09-05 18:28:22 +02:00
Eric Mark Martin
eb6154f792 [ty] add doc-comments for some variance stuff (#20189) 2025-09-05 15:03:10 +01:00
David Peter
fdfb51b595 [ty] Update mypy_primer, add egglog-python project (#20078)
Now that https://github.com/astral-sh/ruff/pull/20263 is merged, we can
update mypy_primer and add the new `egglog-python` project to
`good.txt`. The ecosystem-analyzer run shows that we now add 1,356
diagnostics (where we had over 5,000 previously, due to the unsupported
project layout).
2025-09-05 14:17:07 +02:00
David Peter
7ee863b6d7 [ty] Include python folder in environment.root if it exists (#20263)
## Summary

I felt it was safer to add the `python` folder *in addition* to a
possibly-existing `src` folder, even though the `src` folder only
contains Rust code for `maturin`-based projects. There might be
non-maturin projects where a `python` folder exists for other reasons,
next to a normal `src` layout.

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

## Test Plan

Tested locally on the egglog-python project.
2025-09-05 13:53:48 +02:00
David Peter
8ade6c4eaf [ty] Add backreferences to TypedDict items in diagnostics (#20262)
## Summary

Add backreferences to the original item declaration in TypedDict
diagnostics.

Thanks to @AlexWaygood for the suggestion.

## Test Plan

Updated snapshots
2025-09-05 12:38:37 +02:00
David Peter
9e45bfa9fd [ty] Cover full range of annotated assignments (#20261)
## Summary

An annotated assignment `name: annotation` without a right-hand side was
previously not covered by the range returned from
`DefinitionKind::full_range`, because we did expand the range to include
the right-hand side (if there was one), but failed to include the
annotation.

## Test Plan

Updated snapshot tests
2025-09-05 10:12:40 +02:00
David Peter
7509d376eb [ty] Minor: 'can not' => cannot (#20260) 2025-09-05 09:19:14 +02:00
David Peter
a24a4b55ee [ty] TypedDict: Add support for typing.ReadOnly (#20241)
## Summary

Add support for `typing.ReadOnly` as a type qualifier to mark
`TypedDict` fields as being read-only. If you try to mutate them, you
get a new diagnostic:

<img width="787" height="234" alt="image"
src="https://github.com/user-attachments/assets/f62fddf9-4961-4bcd-ad1c-747043ebe5ff"
/>


## Test Plan

* New Markdown tests
* The typing conformance changes are all correct. There are some false
negatives, but those are related to the missing support for the
functional form of `TypedDict`, or to overriding of fields via
inheritance. Both of these topics are tracked in
https://github.com/astral-sh/ty/issues/154
2025-09-04 15:37:42 -07:00
Alex Waygood
888a22e849 [ty] Reduce false positives for ParamSpecs and TypeVarTuples (#20239) 2025-09-04 23:34:37 +01:00
Jelle Zijlstra
08c1d3660c [ty] Narrow specialized generics using isinstance() (#20256)
Closes astral-sh/ty#456. Part of astral-sh/ty#994.

After all the foundational work, this is only a small change, but let's
see if it exposes any unresolved issues.
2025-09-04 15:28:33 -07:00
Glyphack
0b19caedec Update mdtests 2025-09-05 00:02:39 +02:00
Takayuki Maeda
670fffef37 [ruff] Use helper function for empty f-string detection in in-empty-collection (RUF060) (#20249)
## Summary

Fixes #20238

Replace inline f-string emptiness check with `is_empty_f_string` helper
function
2025-09-04 20:20:59 +00:00
Glyphack
178f48fc0b Update way to identify self in signature 2025-09-04 22:05:05 +02:00
Glyphack
f34b6d8245 Don't display the implicit typing.Self type 2025-09-04 22:01:55 +02:00
Glyphack
8dd183e55c Assume type of self is typing.Self in signature 2025-09-04 22:01:55 +02:00
Jelle Zijlstra
de63f408b9 [ty] Attribute access on top/bottom materializations (#20221)
## Summary

Part of astral-sh/ty#994. The goal of this PR was to add correct
behavior for attribute access on the top and bottom materializations.
This is necessary for the end goal of using the top materialization for
narrowing generics (`isinstance(x, list)`): we want methods like
`x.append` to work correctly in that case.

It turned out to be convenient to represent materialization as a
TypeMapping, so it can be stashed in the `type_mappings` list of a
function object. This also allowed me to remove most concrete
`materialize` methods, since they usually just delegate to the subparts
of the type, the same as other type mappings. That is why the net effect
of this PR is to remove a few hundred lines.

## Test Plan

I added a few more tests. Much of this PR is refactoring and covered by
existing tests.

## Followups

Assigning to attributes of top materializations is not yet covered. This
seems less important so I'd like to defer it.

I noticed that the `materialize` implementation of `Parameters` was
wrong; it did the same for the top and bottom materializations. This PR
makes the bottom materialization slightly more reasonable, but
implementing this correctly will require extending the struct.
2025-09-04 12:01:44 -07:00
Alex Waygood
555b9f78d6 [ty] Minor cleanups (#20240)
## Summary

Two minor cleanups:
- Return `Option<ClassType>` rather than `Option<ClassLiteral>` from
`TypeInferenceBuilder::class_context_of_current_method`. Now that
`ClassType::is_protocol` exists as a method as well as
`ClassLiteral::is_protocol`, this simplifies most of the call-sites of
the `class_context_of_current_method()` method.
- Make more use of the `MethodDecorator::try_from_fn_type` method in
`class.rs`. Under the hood, this method uses the new methods
`FunctionType::is_classmethod()` and `FunctionType::is_staticmethod()`
that @sharkdp recently added, so it gets the semantics more precisely
correct than the code it's replacing in `infer.rs` (by accounting for
implicit staticmethods/classmethods as well as explicit ones). By using
these methods we can delete some code elsewhere (the
`FunctionDecorators::from_decorator_types()` constructor)

## Test Plan

Existing tests
2025-09-04 10:25:49 -07:00
Dylan
c6516e9b60 Bump 0.12.12 (#20242) 2025-09-04 11:35:56 -05:00
David Peter
1aaa0847ab [ty] More tests for TypedDict (#20205)
## Summary

A small set of additional tests for `TypedDict` that I wrote while going
through the spec. Note that this certainly doesn't make the test suite
exhaustive (see remaining open points in the updated list here:
https://github.com/astral-sh/ty/issues/154).
2025-09-04 15:55:42 +00:00
Charlie Marsh
b49aa35074 Split LICENSE addendum by derivation type (#20222)
## Summary

Per
https://github.com/astral-sh/ruff/issues/20191#issuecomment-3251131478,
this PR restructures the license file to draw a distinction between
projects from which we've (e.g.) drawn source code and projects whose
rules we've implemented but have otherwise not reused or adapted source
code from, which are credited in the README. While I was here, I also
sorted the list.
2025-09-03 21:35:33 -04:00
Samuel Rigaud
1e34f3f20a [ty] Fix small test typo (#20220)
Small typo in the comment of a test

Co-authored-by: Samuel Rigaud <rigaud@gmail.com>
2025-09-03 15:24:17 -07:00
Douglas Creager
77b2cee223 [ty] Add functions for revealing assignability/subtyping constraints (#20217)
This PR adds two new `ty_extensions` functions,
`reveal_when_assignable_to` and `reveal_when_subtype_of`. These are
closely related to the existing `is_assignable_to` and `is_subtype_of`,
but instead of returning when the property (always) holds, it produces a
diagnostic that describes _when_ the property holds. (This will let us
construct mdtests that print out constraints that are not always true or
always false — though we don't currently have any instances of those.)

I did not replace _every_ occurrence of the `is_property` variants in
the mdtest suite, instead focusing on the generics-related tests where
it will be important to see the full detail of the constraint sets.

As part of this, I also updated the mdtest harness to accept the shorter
`# revealed:` assertion format for more than just `reveal_type`, and
updated the existing uses of `reveal_protocol_interface` to take
advantage of this.
2025-09-03 16:44:35 -04:00
Dan Parizher
200349c6e8 [flake8-comprehensions] Skip C417 when lambda contains yield/yield from (#20201)
## Summary

Fixes #20198
2025-09-03 16:39:11 -04:00
David Peter
0d4f7dde99 [ty] Treat __new__ as a static method (#20212)
## Summary

Pull this out of https://github.com/astral-sh/ruff/pull/18473 as an
isolated change to make sure it has no adverse effects.

The wrong behavior is observable on `main` for something like
```py
class C:
    def __new__(cls) -> "C":
        cls.x = 1

C.x  # previously: Attribute `x` can only be accessed on instances
     # now:        Type `<class 'C'>` has no attribute `x`
```
where we currently treat `x` as an *instance* attribute (because we
consider `__new__` to be a normal function and `cls` to be the "self"
attribute). With this PR, we do not consider `x` to be an attribute,
neither on the class nor on instances of `C`. If this turns out to be an
important feature, we should add it intentionally, instead of
accidentally.

## Test Plan

Ecosystem checks.
2025-09-03 19:55:20 +02:00
Shahar Naveh
cb1ba0d4c2 Expose Indentation in ruff_python_codegen (#20216)
## Summary

I'm trying to reduce code complexity for
[RustPython](https://github.com/RustPython/RustPython), we have this
file:
056795eed4/compiler/codegen/src/unparse.rs
which can be replaced entirely by `ruff_python_codegen::Generator`.
Unfortunately we can not create an instance of `Generator` easily,
because `Indentation` is not exported at
cda376afe0/crates/ruff_python_codegen/src/lib.rs (L3)

I have managed to bypass this restriction by doing:
```rust
let contents = r"x = 1";
let module = ruff_python_parser::parse_module(contents).unwrap();
let stylist = ruff_python_codegen::Stylist::from_tokens(module.tokens(), contents);
stylist.indentation()
```

But ideally I'd rather use:
```rust
ruff_python_codegen::Indentation::default()
```
2025-09-03 13:32:31 -04:00
Renkai Ge
cda376afe0 [ty]eliminate definitely-impossible types from union in equality narrowing (#20164)
solves https://github.com/astral-sh/ty/issues/939

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-09-03 08:34:22 -07:00
renovate[bot]
b14fc96141 Update Rust crate tracing-subscriber to v0.3.20 (#20162)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tracing-subscriber](https://tokio.rs)
([source](https://redirect.github.com/tokio-rs/tracing)) |
workspace.dependencies | patch | `0.3.19` -> `0.3.20` |

---

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

### GitHub Vulnerability Alerts

####
[CVE-2025-58160](https://redirect.github.com/tokio-rs/tracing/security/advisories/GHSA-xwfj-jgwm-7wp5)

### Impact

Previous versions of tracing-subscriber were vulnerable to ANSI escape
sequence injection attacks. Untrusted user input containing ANSI escape
sequences could be injected into terminal output when logged,
potentially allowing attackers to:

- Manipulate terminal title bars
- Clear screens or modify terminal display
- Potentially mislead users through terminal manipulation

In isolation, impact is minimal, however security issues have been found
in terminal emulators that enabled an attacker to use ANSI escape
sequences via logs to exploit vulnerabilities in the terminal emulator.

### Patches

`tracing-subscriber` version 0.3.20 fixes this vulnerability by escaping
ANSI control characters in when writing events to destinations that may
be printed to the terminal.

### Workarounds

Avoid printing logs to terminal emulators without escaping ANSI control
sequences.

### References

https://www.packetlabs.net/posts/weaponizing-ansi-escape-sequences/

### Acknowledgments

We would like to thank [zefr0x](http://github.com/zefr0x) who
responsibly reported the issue at `security@tokio.rs`.

If you believe you have found a security vulnerability in any tokio-rs
project, please email us at `security@tokio.rs`.

---

### Release Notes

<details>
<summary>tokio-rs/tracing (tracing-subscriber)</summary>

###
[`v0.3.20`](https://redirect.github.com/tokio-rs/tracing/releases/tag/tracing-subscriber-0.3.20):
tracing-subscriber 0.3.20

[Compare
Source](https://redirect.github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20)

**Security Fix**: ANSI Escape Sequence Injection (CVE-TBD)

#### Impact

Previous versions of tracing-subscriber were vulnerable to ANSI escape
sequence injection attacks. Untrusted user input containing ANSI escape
sequences could be injected into terminal output when logged,
potentially allowing attackers to:

- Manipulate terminal title bars
- Clear screens or modify terminal display
- Potentially mislead users through terminal manipulation

In isolation, impact is minimal, however security issues have been found
in terminal emulators that enabled an attacker to use ANSI escape
sequences via logs to exploit vulnerabilities in the terminal emulator.

#### Solution

Version 0.3.20 fixes this vulnerability by escaping ANSI control
characters in when writing events to destinations that may be printed to
the terminal.

#### Affected Versions

All versions of tracing-subscriber prior to 0.3.20 are affected by this
vulnerability.

#### Recommendations

Immediate Action Required: We recommend upgrading to tracing-subscriber
0.3.20 immediately, especially if your application:

- Logs user-provided input (form data, HTTP headers, query parameters,
etc.)
- Runs in environments where terminal output is displayed to users

#### Migration

This is a patch release with no breaking API changes. Simply update your
Cargo.toml:

```toml
[dependencies]
tracing-subscriber = "0.3.20"
```

#### Acknowledgments

We would like to thank [zefr0x](http://github.com/zefr0x) who
responsibly reported the issue at `security@tokio.rs`.

If you believe you have found a security vulnerability in any tokio-rs
project, please email us at `security@tokio.rs`.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (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:eyJjcmVhdGVkSW5WZXIiOiI0MS44Mi43IiwidXBkYXRlZEluVmVyIjoiNDEuODIuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiLCJzZWN1cml0eSJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 10:48:38 -04:00
Wei Lee
c452a2cb79 [airflow] Move airflow.operators.postgres_operator.Mapping from AIR302 to AIR301 (#20172)
<!--
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? -->

### Why
Removal should be grouped into the same category. It doesn't matter
whether it's from a provider or not (and the only case we used to have
was not anyway).
`ProviderReplacement` is used to indicate that we have a replacement and
we might need to install an extra Python package to cater to it.

### What
Move `airflow.operators.postgres_operator.Mapping` from AIR302 to AIR301
and get rid of `ProviderReplace::None`

## Test Plan

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

Update the test fixtures accordingly in the first commit and reorganize
them in the second commit
2025-09-03 10:18:17 -04:00
Bhuminjay Soni
4c3e1930f6 [syntax-errors] Detect yield from inside async function (#20051)
<!--
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

This PR implements
https://docs.astral.sh/ruff/rules/yield-from-in-async-function/ as a
syntax semantic error

## Test Plan

<!-- How was it tested? -->
I have written a simple inline test as directed in
[https://github.com/astral-sh/ruff/issues/17412](https://github.com/astral-sh/ruff/issues/17412)

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-09-03 10:13:05 -04:00
Wei Lee
5d7c17c20a [airflow] Convert DatasetOrTimeSchedule(datasets=...) to AssetOrTimeSchedule(assets=...) (AIR311) (#20202)
<!--
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? -->

update the argument `datasets` as `assets`

## Test Plan

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

update fixture accordingly
2025-09-03 10:12:11 -04:00
Andrew Gallant
c402bf8ae2 [ty] Correct default value for experimental rename setting
Ref https://github.com/astral-sh/ruff/pull/20207#discussion_r2318336964
2025-09-03 09:57:26 -04:00
Andrew Gallant
6bc33a041f [ty] Pick up typed text as a query for unimported completions
It's almost certainly bad juju to show literally every single possible
symbol when completions are requested but there is nothing typed yet.
Moreover, since there are so many symbols, it is likely beneficial to
try and winnow them down before sending them to the client.

This change tries to extract text that has been typed and then uses
that as a query to listing all available symbols.
2025-09-03 09:57:26 -04:00
Andrew Gallant
0a0eaf5a9b [ty] Make auto-import completions opt-in via an experimental option
Instead of waiting to land auto-import until it is "ready
for users," it'd be nicer to get incremental progress merged
to `main`. By making it an experimental opt-in, we avoid making
the default completion experience worse but permit developers
and motivated users to try it.
2025-09-03 09:57:26 -04:00
Andrew Gallant
8e52027a88 [ty] Add naive implementation of completions for unimported symbols
This re-works the `all_symbols` based added previously to work across
all modules available, and not just what is directly in the workspace.

Note that we always pass an empty string as a query, which makes the
results always empty. We'll fix this in a subsequent commit.
2025-09-03 09:57:26 -04:00
Andrew Gallant
78db56e362 [ty] Add base for "all symbols" implementation
This just copies the existing "workspace symbols" implementation and
re-names some things.
2025-09-03 09:57:26 -04:00
Andrew Gallant
046893c186 [ty] Make Module::all_submodules return Module instead of Name
This is to facilitate recursive traversal of all modules in an
environment. This way, we can keep asking for submodules.

This also simplifies how this is used in completions, and probably makes
it faster. Namely, since we return the `Module` itself, callers don't
need to invoke the full module resolver just to get the module type.

Note that this doesn't include namespace packages. (Which were
previously not supported in `Module::all_submodules`.) Given how they
can be spread out across multiple search paths, they will likely require
special consideration here.
2025-09-03 09:57:26 -04:00
Andrew Gallant
9cea752934 [ty] Require that we can find a root when listing sub-modules
This is similar to a change made in the "list top-level modules"
implementation that had been masked by poor Salsa failure modes.
Basically, if we can't find a root here, it *must* be a bug. And if we
just silently skip over it, we risk voiding Salsa's purity contract,
leading to more difficult to debug panics.

This did cause one test to fail, but only because the test wasn't
properly setting up roots.
2025-09-03 09:57:26 -04:00
Wei Lee
3b913ce652 [airflow] Improve the AIR002 error message (#20173)
<!--
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? -->


### What
Change the message from "DAG should have an explicit `schedule`
argument" to "`DAG` or `@dag` should have an explicit `schedule`
argument"

### Why
We're trying to get rid of the idea that DAG in airflow was Directed
acyclic graph. Thus, change it to refer to the class `DAG` or the
decorator `@dag` might help a bit.

## Test Plan

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

update the test fixtures accordly
2025-09-03 09:22:56 -04:00
Brent Westbrook
aee9350df1 [ty] Add GitLab output format (#20155)
## Summary

This wires up the GitLab output format moved into `ruff_db` in
https://github.com/astral-sh/ruff/pull/20117 to the ty CLI.

While I was here, I made one unrelated change to the CLI docs. Clap was
rendering the escapes around the `\[default\]` brackets for the `full`
output, so I just switched those to parentheses:

```
--output-format <OUTPUT_FORMAT>
    The format to use for printing diagnostic messages

    Possible values:
    - full:    Print diagnostics verbosely, with context and helpful hints \[default\]
    - concise: Print diagnostics concisely, one per line
    - gitlab:  Print diagnostics in the JSON format expected by GitLab Code Quality reports
```

## Test Plan

New CLI test, and a manual test with `--config 'terminal.output-format =
"gitlab"'` to make sure this works as a configuration option too. I also
tried piping the output through jq to make sure it's at least valid JSON
2025-09-03 09:08:12 -04:00
David Peter
4e97b97a76 [ty] Run ecosystem-analyzer in release mode (#20210)
## Summary

We already have `mypy_primer` running in debug mode, so this should
provide some additional coverage, and allows us produce reasonable
timing results.

## Test Plan

CI run on this PR
2025-09-03 12:51:30 +02:00
David Peter
00214fc60c [ty] Update ecosystem-analyzer (#20209)
This pulls in some new changes, like the possibility to run on failing
projects (timeouts, abnormal exits).
2025-09-03 12:28:34 +02:00
Aria Desires
ec5584219e [ty] Make initializer calls GotoTargets (#20014)
This introduces `GotoTarget::Call` that represents the kind of
ambiguous/overloaded click of a callable-being-called:

```py
x = mymodule.MyClass(1, 2)
             ^^^^^^^
```

This is equivalent to `GotoTarget::Expression` for the same span but
enriched
with information about the actual callable implementation.

That is, if you click on `MyClass` in `MyClass()` it is *both* a
reference to the class and to the initializer of the class. Therefore
it would be ideal for goto-* and docstrings to be some intelligent
merging of both the class and the initializer.

In particular the callable-implementation (initializer) is prioritized
over the callable-itself (class) so when showing docstrings we will
preferentially show the docs of the initializer if it exists, and then
fallback to the docs of the class.

For goto-definition/goto-declaration we will yield both the class and
the initializer, requiring you to pick which you want (this is perhaps
needlessly pedantic but...).

Fixes https://github.com/astral-sh/ty/issues/898
Fixes https://github.com/astral-sh/ty/issues/1010
2025-09-02 14:49:14 -04:00
chiri
d5e48a0f80 [flake8-use-pathlib] Make PTH119 and PTH120 fixes unsafe because they can change behavior (#20118)
## Summary

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

## Test Plan
`cargo nextest run flake8_use_pathlib`

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-09-02 10:55:24 -05:00
Aria Desires
f40a0b3800 [ty] add more lsp tests for overloads (#20148)
I decided to split out the addition of these tests from other PRs so
that it's easier to follow changes to the LSP's function call handling.
I'm not particularly concerned with whether the results produced by
these tests are "good" or "bad" in this PR, I'm just establishing a
baseline.
2025-09-02 10:38:26 -04:00
200 changed files with 8842 additions and 3444 deletions

View File

@@ -259,6 +259,10 @@ jobs:
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: ty mdtests (GitHub annotations)
if: ${{ needs.determine_changes.outputs.ty == 'true' }}
env:
@@ -317,6 +321,10 @@ jobs:
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
@@ -340,6 +348,10 @@ jobs:
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
@@ -441,9 +453,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@2bb61346d075e720d4c3da92f23b6d612d5a7543 # v1.15.3
with:
tool: cargo-fuzz@0.11.2
uses: cargo-bins/cargo-binstall@837578dfb436769f1e6669b2e23ffea9d9d2da8f # v1.15.4
- name: "Install cargo-fuzz"
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
@@ -463,7 +473,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
@@ -664,7 +674,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -694,7 +704,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@2bb61346d075e720d4c3da92f23b6d612d5a7543 # v1.15.3
- uses: cargo-bins/cargo-binstall@837578dfb436769f1e6669b2e23ffea9d9d2da8f # v1.15.4
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -734,7 +744,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
@@ -777,7 +787,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -909,7 +919,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: "Install Rust toolchain"
run: rustup show
@@ -942,7 +952,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: "Install Rust toolchain"
run: rustup show

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -39,7 +39,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -82,7 +82,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels-*

View File

@@ -65,7 +65,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Sync typeshed stubs
run: |
rm -rf "ruff/${VENDORED_TYPESHED}"
@@ -117,7 +117,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -155,7 +155,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Setup git
run: |
git config --global user.name typeshedbot

View File

@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -64,11 +64,12 @@ 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@1f560d07d672effae250e3d271da53d96c5260ff"
ecosystem-analyzer \
--repository ruff \
diff \
--profile=release \
--projects-old ruff/projects_old.txt \
--projects-new ruff/projects_new.txt \
--old old_commit \

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -1,5 +1,33 @@
# Changelog
## 0.12.12
### Preview features
- Show fixes by default ([#19919](https://github.com/astral-sh/ruff/pull/19919))
- \[`airflow`\] Convert `DatasetOrTimeSchedule(datasets=...)` to `AssetOrTimeSchedule(assets=...)` (`AIR311`) ([#20202](https://github.com/astral-sh/ruff/pull/20202))
- \[`airflow`\] Improve the `AIR002` error message ([#20173](https://github.com/astral-sh/ruff/pull/20173))
- \[`airflow`\] Move `airflow.operators.postgres_operator.Mapping` from `AIR302` to `AIR301` ([#20172](https://github.com/astral-sh/ruff/pull/20172))
- \[`flake8-async`\] Implement `blocking-input` rule (`ASYNC250`) ([#20122](https://github.com/astral-sh/ruff/pull/20122))
- \[`flake8-use-pathlib`\] Make `PTH119` and `PTH120` fixes unsafe because they can change behavior ([#20118](https://github.com/astral-sh/ruff/pull/20118))
- \[`pylint`\] Add U+061C to `PLE2502` ([#20106](https://github.com/astral-sh/ruff/pull/20106))
- \[`ruff`\] Fix false negative for empty f-strings in `deque` calls (`RUF037`) ([#20109](https://github.com/astral-sh/ruff/pull/20109))
### Bug fixes
- Less confidently mark f-strings as empty when inferring truthiness ([#20152](https://github.com/astral-sh/ruff/pull/20152))
- \[`fastapi`\] Fix false positive for paths with spaces around parameters (`FAST003`) ([#20077](https://github.com/astral-sh/ruff/pull/20077))
- \[`flake8-comprehensions`\] Skip `C417` when lambda contains `yield`/`yield from` ([#20201](https://github.com/astral-sh/ruff/pull/20201))
- \[`perflint`\] Handle tuples in dictionary comprehensions (`PERF403`) ([#19934](https://github.com/astral-sh/ruff/pull/19934))
### Rule changes
- \[`pycodestyle`\] Preserve return type annotation for `ParamSpec` (`E731`) ([#20108](https://github.com/astral-sh/ruff/pull/20108))
### Documentation
- Add fix safety sections to docs ([#17490](https://github.com/astral-sh/ruff/pull/17490),[#17499](https://github.com/astral-sh/ruff/pull/17499))
## 0.12.11
### Preview features

223
Cargo.lock generated
View File

@@ -257,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.3"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bitvec"
@@ -295,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.10",
"regex-automata",
"serde",
]
@@ -408,9 +408,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.46"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
dependencies = [
"clap_builder",
"clap_derive",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.46"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
dependencies = [
"anstream",
"anstyle",
@@ -461,9 +461,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.45"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
dependencies = [
"heck",
"proc-macro2",
@@ -603,7 +603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -612,7 +612,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1035,7 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -1231,8 +1231,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.10",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -1241,7 +1241,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"ignore",
"walkdir",
]
@@ -1459,7 +1459,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata 0.4.10",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
@@ -1521,7 +1521,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"inotify-sys",
"libc",
]
@@ -1537,9 +1537,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.43.1"
version = "1.43.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
dependencies = [
"console 0.15.11",
"globset",
@@ -1617,7 +1617,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1681,7 +1681,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1728,9 +1728,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24"
[[package]]
name = "js-sys"
version = "0.3.77"
version = "0.3.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1770,9 +1770,9 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libcst"
version = "1.8.2"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae28ddc5b90c3e3146a21d051ca095cbc8d932ad8714cf65ddf71a9abb35684c"
checksum = "052ef5d9fc958a51aeebdf3713573b36c6fd6eed0bf0e60e204d2c0f8cf19b9f"
dependencies = [
"annotate-snippets",
"libcst_derive",
@@ -1785,9 +1785,9 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "1.8.2"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2de5c2f62bcf8a4f7290b1854388b262c4b68f1db1a3ee3ef6d4c1319b00a3"
checksum = "a91a751afee92cbdd59d4bc6754c7672712eec2d30a308f23de4e3287b2929cb"
dependencies = [
"quote",
"syn",
@@ -1809,7 +1809,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"libc",
"redox_syscall",
]
@@ -1850,9 +1850,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.27"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "lsp-server"
@@ -1913,11 +1913,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata 0.1.10",
"regex-automata",
]
[[package]]
@@ -2014,7 +2014,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2026,7 +2026,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2054,7 +2054,7 @@ version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"fsevent-sys",
"inotify",
"kqueue",
@@ -2074,12 +2074,11 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
"overload",
"winapi",
"windows-sys 0.52.0",
]
[[package]]
@@ -2154,12 +2153,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking_lot"
version = "0.12.4"
@@ -2666,7 +2659,7 @@ version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
]
[[package]]
@@ -2688,17 +2681,8 @@ checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.10",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -2709,7 +2693,7 @@ checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
@@ -2718,12 +2702,6 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -2743,13 +2721,13 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.11"
version = "0.12.12"
dependencies = [
"anyhow",
"argfile",
"assert_fs",
"bincode 2.0.1",
"bitflags 2.9.3",
"bitflags 2.9.4",
"cachedir",
"clap",
"clap_complete_command",
@@ -2999,11 +2977,11 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.11"
version = "0.12.12"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.9.3",
"bitflags 2.9.4",
"clap",
"colored 3.0.0",
"fern",
@@ -3108,7 +3086,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.9.3",
"bitflags 2.9.4",
"compact_str",
"get-size2",
"is-macro",
@@ -3196,7 +3174,7 @@ dependencies = [
name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"itertools 0.14.0",
"ruff_python_ast",
"unic-ucd-category",
@@ -3207,7 +3185,7 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.3",
"bitflags 2.9.4",
"bstr",
"compact_str",
"get-size2",
@@ -3232,7 +3210,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"insta",
"is-macro",
"ruff_cache",
@@ -3253,7 +3231,7 @@ dependencies = [
name = "ruff_python_stdlib"
version = "0.0.0"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"unicode-ident",
]
@@ -3337,7 +3315,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.11"
version = "0.12.12"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3430,11 +3408,11 @@ version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -3827,7 +3805,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4161,15 +4139,15 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"chrono",
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -4240,7 +4218,8 @@ dependencies = [
name = "ty_ide"
version = "0.0.0"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
"camino",
"get-size2",
"insta",
"itertools 0.14.0",
@@ -4278,7 +4257,7 @@ dependencies = [
"pep440_rs",
"rayon",
"regex",
"regex-automata 0.4.10",
"regex-automata",
"ruff_cache",
"ruff_db",
"ruff_macros",
@@ -4304,7 +4283,7 @@ name = "ty_python_semantic"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.3",
"bitflags 2.9.4",
"bitvec",
"camino",
"colored 3.0.0",
@@ -4357,7 +4336,7 @@ name = "ty_server"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.3",
"bitflags 2.9.4",
"crossbeam",
"dunce",
"insta",
@@ -4400,7 +4379,7 @@ name = "ty_test"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.3",
"bitflags 2.9.4",
"camino",
"colored 3.0.0",
"insta",
@@ -4759,21 +4738,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
dependencies = [
"bumpalo",
"log",
@@ -4785,9 +4765,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.50"
version = "0.4.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
dependencies = [
"cfg-if",
"js-sys",
@@ -4798,9 +4778,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4808,9 +4788,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
dependencies = [
"proc-macro2",
"quote",
@@ -4821,18 +4801,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
version = "0.3.50"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad"
dependencies = [
"js-sys",
"minicov",
@@ -4843,9 +4823,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.50"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d"
dependencies = [
"proc-macro2",
"quote",
@@ -4854,9 +4834,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.77"
version = "0.3.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4892,37 +4872,15 @@ dependencies = [
"glob",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -4982,6 +4940,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -5150,7 +5117,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.4",
]
[[package]]

View File

@@ -115,7 +115,7 @@ jiff = { version = "0.2.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "1.0.0" }
libc = { version = "0.2.153" }
libcst = { version = "1.1.0", default-features = false }
libcst = { version = "1.8.4", default-features = false }
log = { version = "0.4.17" }
lsp-server = { version = "0.7.6" }
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
@@ -251,6 +251,14 @@ rest_pat_in_fully_bound_structs = "warn"
redundant_clone = "warn"
debug_assert_with_mut_call = "warn"
unused_peekable = "warn"
# This lint sometimes flags code whose `if` and `else`
# bodies could be flipped when a `!` operator is removed.
# While perhaps sometimes a good idea, it is also often
# not a good idea due to other factors impacting
# readability. For example, if flipping the bodies results
# in the `if` being an order of magnitude bigger than the
# `else`, then some might consider that harder to read.
if_not_else = "allow"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"

1160
LICENSE

File diff suppressed because it is too large Load Diff

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.12.11/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.11/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.12/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.12/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.12.11
rev: v0.12.12
hooks:
# Run the linter.
- id: ruff-check

View File

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

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

@@ -454,24 +454,26 @@ impl Diagnostic {
/// Computes the start source location for the message.
///
/// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the
/// span has no range.
pub fn expect_ruff_start_location(&self) -> LineColumn {
self.expect_primary_span()
.expect_ruff_file()
.to_source_code()
.line_column(self.expect_range().start())
/// Returns None if the diagnostic has no primary span, if its file is not a `SourceFile`,
/// or if the span has no range.
pub fn ruff_start_location(&self) -> Option<LineColumn> {
Some(
self.ruff_source_file()?
.to_source_code()
.line_column(self.range()?.start()),
)
}
/// Computes the end source location for the message.
///
/// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the
/// span has no range.
pub fn expect_ruff_end_location(&self) -> LineColumn {
self.expect_primary_span()
.expect_ruff_file()
.to_source_code()
.line_column(self.expect_range().end())
/// Returns None if the diagnostic has no primary span, if its file is not a `SourceFile`,
/// or if the span has no range.
pub fn ruff_end_location(&self) -> Option<LineColumn> {
Some(
self.ruff_source_file()?
.to_source_code()
.line_column(self.range()?.end()),
)
}
/// Returns the [`SourceFile`] which the message belongs to.
@@ -501,13 +503,18 @@ impl Diagnostic {
/// Returns the ordering of diagnostics based on the start of their ranges, if they have any.
///
/// Panics if either diagnostic has no primary span, if the span has no range, or if its file is
/// not a `SourceFile`.
/// Panics if either diagnostic has no primary span, or if its file is not a `SourceFile`.
pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
(self.expect_ruff_source_file(), self.expect_range().start()).cmp(&(
let a = (
self.expect_ruff_source_file(),
self.range().map(|r| r.start()),
);
let b = (
other.expect_ruff_source_file(),
other.expect_range().start(),
))
other.range().map(|r| r.start()),
);
a.cmp(&b)
}
}
@@ -1444,7 +1451,7 @@ pub enum DiagnosticFormat {
Junit,
/// Print diagnostics in the JSON format used by GitLab [Code Quality] reports.
///
/// [Code Quality]: https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
/// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
#[cfg(feature = "serde")]
Gitlab,
}

View File

@@ -81,14 +81,19 @@ impl IndentStyle {
pub const fn is_space(&self) -> bool {
matches!(self, IndentStyle::Space)
}
/// Returns the string representation of the indent style.
pub const fn as_str(&self) -> &'static str {
match self {
IndentStyle::Tab => "tab",
IndentStyle::Space => "space",
}
}
}
impl std::fmt::Display for IndentStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IndentStyle::Tab => std::write!(f, "tab"),
IndentStyle::Space => std::write!(f, "space"),
}
f.write_str(self.as_str())
}
}

View File

@@ -139,4 +139,16 @@ impl LineEnding {
LineEnding::CarriageReturn => "\r",
}
}
/// Returns the string used to configure this line ending.
///
/// See [`LineEnding::as_str`] for the actual string representation of the line ending.
#[inline]
pub const fn as_setting_str(&self) -> &'static str {
match self {
LineEnding::LineFeed => "lf",
LineEnding::CarriageReturnLineFeed => "crlf",
LineEnding::CarriageReturn => "cr",
}
}
}

View File

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

View File

@@ -12,6 +12,7 @@ from airflow import (
from airflow.api_connexion.security import requires_access
from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.datasets import DatasetAliasEvent
from airflow.operators.postgres_operator import Mapping
from airflow.operators.subdag import SubDagOperator
from airflow.secrets.cache import SecretCache
from airflow.secrets.local_filesystem import LocalFilesystemBackend
@@ -52,6 +53,8 @@ DatasetAliasEvent()
# airflow.operators.subdag.*
SubDagOperator()
# airflow.operators.postgres_operator
Mapping()
# airflow.secrets
# get_connection

View File

@@ -70,7 +70,7 @@ from airflow.timetables.datasets import DatasetOrTimeSchedule
from airflow.utils.dag_parsing_context import get_parsing_context
# airflow.timetables.datasets
DatasetOrTimeSchedule()
DatasetOrTimeSchedule(datasets=[])
# airflow.utils.dag_parsing_context
get_parsing_context()

View File

@@ -75,3 +75,7 @@ list(map(lambda x, y: x, [(1, 2), (3, 4)]))
_ = t"{set(map(lambda x: x % 2 == 0, nums))}"
_ = t"{dict(map(lambda v: (v, v**2), nums))}"
# See https://github.com/astral-sh/ruff/issues/20198
# No error: lambda contains `yield`, so map() should not be rewritten
map(lambda x: (yield x), [1, 2, 3])

View File

@@ -141,3 +141,133 @@ class ExampleWithKeywords:
def method3(self):
super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
# See: https://github.com/astral-sh/ruff/issues/19357
# Must be detected
class ParentD:
def f(self):
print("D")
class ChildD1(ParentD):
def f(self):
if False: __class__ # Python injects __class__ into scope
builtins.super(ChildD1, self).f()
class ChildD2(ParentD):
def f(self):
if False: super # Python injects __class__ into scope
builtins.super(ChildD2, self).f()
class ChildD3(ParentD):
def f(self):
builtins.super(ChildD3, self).f()
super # Python injects __class__ into scope
import builtins as builtins_alias
class ChildD4(ParentD):
def f(self):
builtins_alias.super(ChildD4, self).f()
super # Python injects __class__ into scope
class ChildD5(ParentD):
def f(self):
super = 1
super # Python injects __class__ into scope
builtins.super(ChildD5, self).f()
class ChildD6(ParentD):
def f(self):
super: "Any"
__class__ # Python injects __class__ into scope
builtins.super(ChildD6, self).f()
class ChildD7(ParentD):
def f(self):
def x():
__class__ # Python injects __class__ into scope
builtins.super(ChildD7, self).f()
class ChildD8(ParentD):
def f(self):
def x():
super = 1
super # Python injects __class__ into scope
builtins.super(ChildD8, self).f()
class ChildD9(ParentD):
def f(self):
def x():
__class__ = 1
__class__ # Python injects __class__ into scope
builtins.super(ChildD9, self).f()
class ChildD10(ParentD):
def f(self):
def x():
__class__ = 1
super # Python injects __class__ into scope
builtins.super(ChildD10, self).f()
# Must be ignored
class ParentI:
def f(self):
print("I")
class ChildI1(ParentI):
def f(self):
builtins.super(ChildI1, self).f() # no __class__ in the local scope
class ChildI2(ParentI):
def b(self):
x = __class__
if False: super
def f(self):
self.b()
builtins.super(ChildI2, self).f() # no __class__ in the local scope
class ChildI3(ParentI):
def f(self):
if False: super
def x(_):
builtins.super(ChildI3, self).f() # no __class__ in the local scope
x(None)
class ChildI4(ParentI):
def f(self):
super: "str"
builtins.super(ChildI4, self).f() # no __class__ in the local scope
class ChildI5(ParentI):
def f(self):
super = 1
__class__ = 3
builtins.super(ChildI5, self).f() # no __class__ in the local scope
class ChildI6(ParentI):
def f(self):
__class__ = None
__class__
builtins.super(ChildI6, self).f() # no __class__ in the local scope
class ChildI7(ParentI):
def f(self):
__class__ = None
super
builtins.super(ChildI7, self).f()
class ChildI8(ParentI):
def f(self):
__class__: "Any"
super
builtins.super(ChildI8, self).f()
class ChildI9(ParentI):
def f(self):
class A:
def foo(self):
if False: super
if False: __class__
builtins.super(ChildI9, self).f()

View File

@@ -0,0 +1,59 @@
from collections.abc import Generator, AsyncGenerator
def func() -> Generator[int, None, None]:
yield 42
def func() -> Generator[int, None]:
yield 42
def func() -> Generator[int]:
yield 42
def func() -> Generator[int, int, int]:
foo = yield 42
return foo
def func() -> Generator[int, int, None]:
_ = yield 42
return None
def func() -> Generator[int, None, int]:
yield 42
return 42
async def func() -> AsyncGenerator[int, None]:
yield 42
async def func() -> AsyncGenerator[int]:
yield 42
async def func() -> AsyncGenerator[int, int]:
foo = yield 42
return foo
from typing import Generator, AsyncGenerator
def func() -> Generator[str, None, None]:
yield "hello"
async def func() -> AsyncGenerator[str, None]:
yield "hello"
async def func() -> AsyncGenerator[ # type: ignore
str,
None
]:
yield "hello"

View File

@@ -42,3 +42,7 @@ b"a" in bytes("a", "utf-8")
1 in set(set([1]))
'' in {""}
frozenset() in {frozenset()}
# https://github.com/astral-sh/ruff/issues/20238
"b" in f"" "" # Error
"b" in f"" "x" # OK

View File

@@ -0,0 +1 @@
async def f(): yield from x # error

View File

@@ -9,6 +9,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::{
is_assert_raises_exception_call_enabled, is_optional_as_none_in_union_enabled,
is_unnecessary_default_type_args_stubs_enabled,
};
use crate::registry::Rule;
use crate::rules::{
@@ -142,7 +143,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
}
if checker.is_rule_enabled(Rule::UnnecessaryDefaultTypeArgs) {
if checker.target_version() >= PythonVersion::PY313 {
if checker.target_version() >= PythonVersion::PY313
|| is_unnecessary_default_type_args_stubs_enabled(checker.settings())
&& checker.semantic().in_stub_file()
{
pyupgrade::rules::unnecessary_default_type_args(checker, expr);
}
}
@@ -1319,13 +1323,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
pylint::rules::yield_in_init(checker, expr);
}
}
Expr::YieldFrom(yield_from) => {
Expr::YieldFrom(_) => {
if checker.is_rule_enabled(Rule::YieldInInit) {
pylint::rules::yield_in_init(checker, expr);
}
if checker.is_rule_enabled(Rule::YieldFromInAsyncFunction) {
pylint::rules::yield_from_in_async_function(checker, yield_from);
}
}
Expr::FString(f_string_expr @ ast::ExprFString { value, .. }) => {
if checker.is_rule_enabled(Rule::FStringMissingPlaceholders) {

View File

@@ -71,7 +71,9 @@ use crate::registry::Rule;
use crate::rules::pyflakes::rules::{
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
};
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
use crate::rules::pylint::rules::{
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
};
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
use crate::settings::rule_table::RuleTable;
use crate::settings::{LinterSettings, TargetVersion, flags};
@@ -668,6 +670,12 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(AwaitOutsideAsync, error.range);
}
}
SemanticSyntaxErrorKind::YieldFromInAsyncFunction => {
// PLE1700
if self.is_rule_enabled(Rule::YieldFromInAsyncFunction) {
self.report_diagnostic(YieldFromInAsyncFunction, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)

View File

@@ -1231,6 +1231,10 @@ mod tests {
)]
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async_function.py"))]
#[test_case(Rule::AwaitOutsideAsync, Path::new("async_comprehension.py"))]
#[test_case(
Rule::YieldFromInAsyncFunction,
Path::new("yield_from_in_async_function.py")
)]
fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy().to_string();
let path = Path::new("resources/test/fixtures/syntax_errors").join(path);

View File

@@ -19,7 +19,7 @@ impl Emitter for GithubEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for diagnostic in diagnostics {
let source_location = diagnostic.expect_ruff_start_location();
let source_location = diagnostic.ruff_start_location().unwrap_or_default();
let filename = diagnostic.expect_ruff_filename();
let location = if context.is_notebook(&filename) {
// We can't give a reasonable location for the structured formats,
@@ -29,7 +29,7 @@ impl Emitter for GithubEmitter {
source_location
};
let end_location = diagnostic.expect_ruff_end_location();
let end_location = diagnostic.ruff_end_location().unwrap_or_default();
write!(
writer,

View File

@@ -105,7 +105,7 @@ fn group_diagnostics_by_filename(
.or_insert_with(Vec::new)
.push(MessageWithLocation {
message: diagnostic,
start_location: diagnostic.expect_ruff_start_location(),
start_location: diagnostic.ruff_start_location().unwrap_or_default(),
});
}
grouped_messages

View File

@@ -158,8 +158,8 @@ struct SarifResult<'a> {
impl<'a> SarifResult<'a> {
#[cfg(not(target_arch = "wasm32"))]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.expect_ruff_start_location();
let end_location = message.expect_ruff_end_location();
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),
@@ -178,8 +178,8 @@ impl<'a> SarifResult<'a> {
#[cfg(target_arch = "wasm32")]
#[expect(clippy::unnecessary_wraps)]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.expect_ruff_start_location();
let end_location = message.expect_ruff_end_location();
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),

View File

@@ -260,3 +260,10 @@ pub(crate) const fn is_maxsplit_without_separator_fix_enabled(settings: &LinterS
pub(crate) const fn is_bidi_forbid_arabic_letter_mark_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20027
pub(crate) const fn is_unnecessary_default_type_args_stubs_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}

View File

@@ -37,7 +37,6 @@ pub(crate) enum Replacement {
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ProviderReplacement {
None,
AutoImport {
module: &'static str,
name: &'static str,

View File

@@ -46,7 +46,7 @@ pub(crate) struct AirflowDagNoScheduleArgument;
impl Violation for AirflowDagNoScheduleArgument {
#[derive_message_formats]
fn message(&self) -> String {
"DAG should have an explicit `schedule` argument".to_string()
"`DAG` or `@dag` should have an explicit `schedule` argument".to_string()
}
}

View File

@@ -50,9 +50,6 @@ impl Violation for Airflow3MovedToProvider<'_> {
replacement,
} = self;
match replacement {
ProviderReplacement::None => {
format!("`{deprecated}` is removed in Airflow 3.0")
}
ProviderReplacement::AutoImport {
name: _,
module: _,
@@ -85,7 +82,6 @@ impl Violation for Airflow3MovedToProvider<'_> {
provider,
version,
} => Some((module, name.as_str(), provider, version)),
ProviderReplacement::None => None,
} {
Some(format!(
"Install `apache-airflow-providers-{provider}>={version}` and use `{name}` from `{module}` instead."
@@ -1020,7 +1016,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
provider: "postgres",
version: "1.0.0",
},
["airflow", "operators", "postgres_operator", "Mapping"] => ProviderReplacement::None,
// apache-airflow-providers-presto
["airflow", "hooks", "presto_hook", "PrestoHook"] => ProviderReplacement::AutoImport {
@@ -1209,16 +1204,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => {
(module, name.as_str())
}
ProviderReplacement::None => {
checker.report_diagnostic(
Airflow3MovedToProvider {
deprecated: qualified_name,
replacement,
},
ranged,
);
return;
}
};
if is_guarded_by_try_except(expr, module, name, checker.semantic()) {

View File

@@ -704,6 +704,7 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
["airflow", "operators", "subdag", ..] => {
Replacement::Message("The whole `airflow.subdag` module has been removed.")
}
["airflow", "operators", "postgres_operator", "Mapping"] => Replacement::None,
["airflow", "operators", "python", "get_current_context"] => Replacement::AutoImport {
module: "airflow.sdk",
name: "get_current_context",

View File

@@ -65,9 +65,6 @@ impl Violation for Airflow3SuggestedToMoveToProvider<'_> {
replacement,
} = self;
match replacement {
ProviderReplacement::None => {
format!("`{deprecated}` is removed in Airflow 3.0")
}
ProviderReplacement::AutoImport {
name: _,
module: _,
@@ -91,7 +88,6 @@ impl Violation for Airflow3SuggestedToMoveToProvider<'_> {
fn fix_title(&self) -> Option<String> {
let Airflow3SuggestedToMoveToProvider { replacement, .. } = self;
match replacement {
ProviderReplacement::None => None,
ProviderReplacement::AutoImport {
module,
name,
@@ -319,16 +315,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => {
(module, name.as_str())
}
ProviderReplacement::None => {
checker.report_diagnostic(
Airflow3SuggestedToMoveToProvider {
deprecated: qualified_name,
replacement: replacement.clone(),
},
ranged.range(),
);
return;
}
};
if is_guarded_by_try_except(expr, module, name, checker.semantic()) {

View File

@@ -157,6 +157,9 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
["airflow", .., "DAG" | "dag"] => {
diagnostic_for_argument(checker, arguments, "sla_miss_callback", None);
}
["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => {
diagnostic_for_argument(checker, arguments, "datasets", Some("assets"));
}
segments => {
if is_airflow_builtin_or_provider(segments, "operators", "Operator") {
diagnostic_for_argument(checker, arguments, "sla", None);

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR002 DAG should have an explicit `schedule` argument
AIR002 `DAG` or `@dag` should have an explicit `schedule` argument
--> AIR002.py:4:1
|
2 | from airflow.timetables.simple import NullTimetable
@@ -12,7 +12,7 @@ AIR002 DAG should have an explicit `schedule` argument
6 | DAG(dag_id="class_schedule", schedule="@hourly")
|
AIR002 DAG should have an explicit `schedule` argument
AIR002 `DAG` or `@dag` should have an explicit `schedule` argument
--> AIR002.py:13:2
|
13 | @dag()

View File

@@ -2,350 +2,362 @@
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR301 `airflow.PY36` is removed in Airflow 3.0
--> AIR301_names.py:39:1
--> AIR301_names.py:40:1
|
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^
40 |
41 | # airflow.api_connexion.security
41 |
42 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY37` is removed in Airflow 3.0
--> AIR301_names.py:39:7
--> AIR301_names.py:40:7
|
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^
40 |
41 | # airflow.api_connexion.security
41 |
42 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY38` is removed in Airflow 3.0
--> AIR301_names.py:39:13
--> AIR301_names.py:40:13
|
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^
40 |
41 | # airflow.api_connexion.security
41 |
42 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY39` is removed in Airflow 3.0
--> AIR301_names.py:39:19
--> AIR301_names.py:40:19
|
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^
40 |
41 | # airflow.api_connexion.security
41 |
42 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY310` is removed in Airflow 3.0
--> AIR301_names.py:39:25
--> AIR301_names.py:40:25
|
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^
40 |
41 | # airflow.api_connexion.security
41 |
42 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY311` is removed in Airflow 3.0
--> AIR301_names.py:39:32
--> AIR301_names.py:40:32
|
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^
40 |
41 | # airflow.api_connexion.security
41 |
42 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY312` is removed in Airflow 3.0
--> AIR301_names.py:39:39
--> AIR301_names.py:40:39
|
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^
40 |
41 | # airflow.api_connexion.security
41 |
42 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0
--> AIR301_names.py:42:1
--> AIR301_names.py:43:1
|
41 | # airflow.api_connexion.security
42 | requires_access
42 | # airflow.api_connexion.security
43 | requires_access
| ^^^^^^^^^^^^^^^
43 |
44 | # airflow.contrib.*
44 |
45 | # airflow.contrib.*
|
help: Use `airflow.api_fastapi.core_api.security.requires_access_*` instead
AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0
--> AIR301_names.py:45:1
--> AIR301_names.py:46:1
|
44 | # airflow.contrib.*
45 | AWSAthenaHook()
45 | # airflow.contrib.*
46 | AWSAthenaHook()
| ^^^^^^^^^^^^^
|
help: The whole `airflow.contrib` module has been removed.
AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
--> AIR301_names.py:49:1
--> AIR301_names.py:50:1
|
48 | # airflow.datasets
49 | DatasetAliasEvent()
49 | # airflow.datasets
50 | DatasetAliasEvent()
| ^^^^^^^^^^^^^^^^^
|
AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
--> AIR301_names.py:53:1
--> AIR301_names.py:54:1
|
52 | # airflow.operators.subdag.*
53 | SubDagOperator()
53 | # airflow.operators.subdag.*
54 | SubDagOperator()
| ^^^^^^^^^^^^^^
55 |
56 | # airflow.operators.postgres_operator
|
help: The whole `airflow.subdag` module has been removed.
AIR301 [*] `airflow.secrets.cache.SecretCache` is removed in Airflow 3.0
--> AIR301_names.py:61:1
AIR301 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0
--> AIR301_names.py:57:1
|
60 | # airflow.secrets.cache
61 | SecretCache()
56 | # airflow.operators.postgres_operator
57 | Mapping()
| ^^^^^^^
58 |
59 | # airflow.secrets
|
AIR301 [*] `airflow.secrets.cache.SecretCache` is removed in Airflow 3.0
--> AIR301_names.py:64:1
|
63 | # airflow.secrets.cache
64 | SecretCache()
| ^^^^^^^^^^^
|
help: Use `SecretCache` from `airflow.sdk` instead.
13 | from airflow.contrib.aws_athena_hook import AWSAthenaHook
14 | from airflow.datasets import DatasetAliasEvent
15 | from airflow.operators.subdag import SubDagOperator
15 | from airflow.operators.postgres_operator import Mapping
16 | from airflow.operators.subdag import SubDagOperator
- from airflow.secrets.cache import SecretCache
16 | from airflow.secrets.local_filesystem import LocalFilesystemBackend
17 | from airflow.triggers.external_task import TaskStateTrigger
18 | from airflow.utils import dates
17 | from airflow.secrets.local_filesystem import LocalFilesystemBackend
18 | from airflow.triggers.external_task import TaskStateTrigger
19 | from airflow.utils import dates
--------------------------------------------------------------------------------
33 | from airflow.utils.trigger_rule import TriggerRule
34 | from airflow.www.auth import has_access, has_access_dataset
35 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
36 + from airflow.sdk import SecretCache
37 |
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
34 | from airflow.utils.trigger_rule import TriggerRule
35 | from airflow.www.auth import has_access, has_access_dataset
36 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
37 + from airflow.sdk import SecretCache
38 |
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
note: This is an unsafe fix and may change runtime behavior
AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
--> AIR301_names.py:65:1
|
64 | # airflow.triggers.external_task
65 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^
66 |
67 | # airflow.utils.date
|
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:68:1
|
67 | # airflow.utils.date
68 | dates.date_range
67 | # airflow.triggers.external_task
68 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^
69 | dates.days_ago
69 |
70 | # airflow.utils.date
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:69:1
|
67 | # airflow.utils.date
68 | dates.date_range
69 | dates.days_ago
| ^^^^^^^^^^^^^^
70 |
71 | date_range
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:71:1
|
69 | dates.days_ago
70 |
71 | date_range
| ^^^^^^^^^^
72 | days_ago
73 | infer_time_unit
70 | # airflow.utils.date
71 | dates.date_range
| ^^^^^^^^^^^^^^^^
72 | dates.days_ago
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:72:1
|
71 | date_range
72 | days_ago
70 | # airflow.utils.date
71 | dates.date_range
72 | dates.days_ago
| ^^^^^^^^^^^^^^
73 |
74 | date_range
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:74:1
|
72 | dates.days_ago
73 |
74 | date_range
| ^^^^^^^^^^
75 | days_ago
76 | infer_time_unit
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:75:1
|
74 | date_range
75 | days_ago
| ^^^^^^^^
73 | infer_time_unit
74 | parse_execution_date
76 | infer_time_unit
77 | parse_execution_date
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
--> AIR301_names.py:73:1
--> AIR301_names.py:76:1
|
71 | date_range
72 | days_ago
73 | infer_time_unit
74 | date_range
75 | days_ago
76 | infer_time_unit
| ^^^^^^^^^^^^^^^
74 | parse_execution_date
75 | round_time
77 | parse_execution_date
78 | round_time
|
AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
--> AIR301_names.py:74:1
--> AIR301_names.py:77:1
|
72 | days_ago
73 | infer_time_unit
74 | parse_execution_date
75 | days_ago
76 | infer_time_unit
77 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^
75 | round_time
76 | scale_time_units
78 | round_time
79 | scale_time_units
|
AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
--> AIR301_names.py:75:1
--> AIR301_names.py:78:1
|
73 | infer_time_unit
74 | parse_execution_date
75 | round_time
76 | infer_time_unit
77 | parse_execution_date
78 | round_time
| ^^^^^^^^^^
76 | scale_time_units
79 | scale_time_units
|
AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
--> AIR301_names.py:76:1
--> AIR301_names.py:79:1
|
74 | parse_execution_date
75 | round_time
76 | scale_time_units
77 | parse_execution_date
78 | round_time
79 | scale_time_units
| ^^^^^^^^^^^^^^^^
77 |
78 | # This one was not deprecated.
80 |
81 | # This one was not deprecated.
|
AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
--> AIR301_names.py:83:1
--> AIR301_names.py:86:1
|
82 | # airflow.utils.dag_cycle_tester
83 | test_cycle
85 | # airflow.utils.dag_cycle_tester
86 | test_cycle
| ^^^^^^^^^^
|
AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
--> AIR301_names.py:87:1
--> AIR301_names.py:90:1
|
86 | # airflow.utils.db
87 | create_session
89 | # airflow.utils.db
90 | create_session
| ^^^^^^^^^^^^^^
88 |
89 | # airflow.utils.decorators
91 |
92 | # airflow.utils.decorators
|
AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0
--> AIR301_names.py:90:1
--> AIR301_names.py:93:1
|
89 | # airflow.utils.decorators
90 | apply_defaults
92 | # airflow.utils.decorators
93 | apply_defaults
| ^^^^^^^^^^^^^^
91 |
92 | # airflow.utils.file
94 |
95 | # airflow.utils.file
|
help: `apply_defaults` is now unconditionally done and can be safely removed.
AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
--> AIR301_names.py:93:1
--> AIR301_names.py:96:1
|
92 | # airflow.utils.file
93 | mkdirs
95 | # airflow.utils.file
96 | mkdirs
| ^^^^^^
|
help: Use `pathlib.Path({path}).mkdir` instead
AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
--> AIR301_names.py:97:1
|
96 | # airflow.utils.state
97 | SHUTDOWN
| ^^^^^^^^
98 | terminating_states
|
--> AIR301_names.py:100:1
|
99 | # airflow.utils.state
100 | SHUTDOWN
| ^^^^^^^^
101 | terminating_states
|
AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
--> AIR301_names.py:98:1
--> AIR301_names.py:101:1
|
96 | # airflow.utils.state
97 | SHUTDOWN
98 | terminating_states
99 | # airflow.utils.state
100 | SHUTDOWN
101 | terminating_states
| ^^^^^^^^^^^^^^^^^^
99 |
100 | # airflow.utils.trigger_rule
102 |
103 | # airflow.utils.trigger_rule
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
--> AIR301_names.py:101:1
--> AIR301_names.py:104:1
|
100 | # airflow.utils.trigger_rule
101 | TriggerRule.DUMMY
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
| ^^^^^^^^^^^^^^^^^
102 | TriggerRule.NONE_FAILED_OR_SKIPPED
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
--> AIR301_names.py:102:1
--> AIR301_names.py:105:1
|
100 | # airflow.utils.trigger_rule
101 | TriggerRule.DUMMY
102 | TriggerRule.NONE_FAILED_OR_SKIPPED
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
--> AIR301_names.py:106:1
--> AIR301_names.py:109:1
|
105 | # airflow.www.auth
106 | has_access
108 | # airflow.www.auth
109 | has_access
| ^^^^^^^^^^
107 | has_access_dataset
110 | has_access_dataset
|
AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0
--> AIR301_names.py:107:1
--> AIR301_names.py:110:1
|
105 | # airflow.www.auth
106 | has_access
107 | has_access_dataset
108 | # airflow.www.auth
109 | has_access
110 | has_access_dataset
| ^^^^^^^^^^^^^^^^^^
108 |
109 | # airflow.www.utils
111 |
112 | # airflow.www.utils
|
AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
--> AIR301_names.py:110:1
--> AIR301_names.py:113:1
|
109 | # airflow.www.utils
110 | get_sensitive_variables_fields
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
111 | should_hide_value_for_key
114 | should_hide_value_for_key
|
AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
--> AIR301_names.py:111:1
--> AIR301_names.py:114:1
|
109 | # airflow.www.utils
110 | get_sensitive_variables_fields
111 | should_hide_value_for_key
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
114 | should_hide_value_for_key
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -20,11 +20,3 @@ help: Install `apache-airflow-providers-postgres>=1.0.0` and use `PostgresHook`
6 | PostgresHook()
7 | Mapping()
note: This is an unsafe fix and may change runtime behavior
AIR302 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0
--> AIR302_postgres.py:7:1
|
6 | PostgresHook()
7 | Mapping()
| ^^^^^^^
|

View File

@@ -558,7 +558,7 @@ AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Air
--> AIR311_names.py:73:1
|
72 | # airflow.timetables.datasets
73 | DatasetOrTimeSchedule()
73 | DatasetOrTimeSchedule(datasets=[])
| ^^^^^^^^^^^^^^^^^^^^^
74 |
75 | # airflow.utils.dag_parsing_context
@@ -570,12 +570,31 @@ help: Use `AssetOrTimeSchedule` from `airflow.timetables.assets` instead.
71 + from airflow.timetables.assets import AssetOrTimeSchedule
72 |
73 | # airflow.timetables.datasets
- DatasetOrTimeSchedule()
74 + AssetOrTimeSchedule()
- DatasetOrTimeSchedule(datasets=[])
74 + AssetOrTimeSchedule(datasets=[])
75 |
76 | # airflow.utils.dag_parsing_context
77 | get_parsing_context()
AIR311 [*] `datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:73:23
|
72 | # airflow.timetables.datasets
73 | DatasetOrTimeSchedule(datasets=[])
| ^^^^^^^^
74 |
75 | # airflow.utils.dag_parsing_context
|
help: Use `assets` instead
70 | from airflow.utils.dag_parsing_context import get_parsing_context
71 |
72 | # airflow.timetables.datasets
- DatasetOrTimeSchedule(datasets=[])
73 + DatasetOrTimeSchedule(assets=[])
74 |
75 | # airflow.utils.dag_parsing_context
76 | get_parsing_context()
AIR311 [*] `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:76:1
|
@@ -593,7 +612,7 @@ help: Use `get_parsing_context` from `airflow.sdk` instead.
70 + from airflow.sdk import get_parsing_context
71 |
72 | # airflow.timetables.datasets
73 | DatasetOrTimeSchedule()
73 | DatasetOrTimeSchedule(datasets=[])
note: This is an unsafe fix and may change runtime behavior
AIR311 [*] `airflow.decorators.base.DecoratedMappedOperator` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.

View File

@@ -79,7 +79,6 @@ impl Violation for FastApiUnusedPathParameter {
function_name,
is_positional,
} = self;
#[expect(clippy::if_not_else)]
if !is_positional {
format!(
"Parameter `{arg_name}` appears in route path, but not in `{function_name}` signature"

View File

@@ -122,6 +122,13 @@ pub(crate) fn unnecessary_map(checker: &Checker, call: &ast::ExprCall) {
}
};
// If the lambda body contains a `yield` or `yield from`, rewriting `map(lambda ...)` to a
// generator expression or any comprehension is invalid Python syntax
// (e.g., `yield` is not allowed inside generator or comprehension expressions). In such cases, skip.
if lambda_contains_yield(&lambda.body) {
return;
}
for iterable in iterables {
// For example, (x+1 for x in (c:=a)) is invalid syntax
// so we can't suggest it.
@@ -183,6 +190,13 @@ fn map_lambda_and_iterables<'a>(
Some((lambda, iterables))
}
/// Returns true if the expression tree contains a `yield` or `yield from` expression.
fn lambda_contains_yield(expr: &Expr) -> bool {
any_over_expr(expr, &|expr| {
matches!(expr, Expr::Yield(_) | Expr::YieldFrom(_))
})
}
/// A lambda as the first argument to `map()` has the "expected" arity when:
///
/// * It has exactly one parameter

View File

@@ -342,6 +342,7 @@ help: Replace `map()` with a set comprehension
75 + _ = t"{ {x % 2 == 0 for x in nums} }"
76 | _ = t"{dict(map(lambda v: (v, v**2), nums))}"
77 |
78 |
note: This is an unsafe fix and may change runtime behavior
C417 [*] Unnecessary `map()` usage (rewrite using a dict comprehension)
@@ -359,4 +360,6 @@ help: Replace `map()` with a dict comprehension
- _ = t"{dict(map(lambda v: (v, v**2), nums))}"
76 + _ = t"{ {v: v**2 for v in nums} }"
77 |
78 |
79 | # See https://github.com/astral-sh/ruff/issues/20198
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::{self as ast, ParameterWithDefault};
use ruff_python_semantic::analyze::function_type;
use crate::Violation;
@@ -85,16 +85,9 @@ pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast
function_type::FunctionType::Method | function_type::FunctionType::ClassMethod
));
if let Some(arg) = function_def.parameters.args.get(skip) {
if is_old_style_positional_only(arg) {
checker.report_diagnostic(Pep484StylePositionalOnlyParameter, arg.identifier());
if let Some(param) = function_def.parameters.args.get(skip) {
if param.uses_pep_484_positional_only_convention() {
checker.report_diagnostic(Pep484StylePositionalOnlyParameter, param.identifier());
}
}
}
/// Returns `true` if the [`ParameterWithDefault`] is an old-style positional-only parameter (i.e.,
/// its name starts with `__` and does not end with `__`).
fn is_old_style_positional_only(param: &ParameterWithDefault) -> bool {
let arg_name = param.name();
arg_name.starts_with("__") && !arg_name.ends_with("__")
}

View File

@@ -60,6 +60,7 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
fn_argument: &str,
fix_enabled: bool,
violation: impl Violation,
applicability: Option<Applicability>,
) {
if call.arguments.len() != 1 {
return;
@@ -74,33 +75,39 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
if fix_enabled {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.{attr}")
} else {
format!("{binding}({arg_code}).{attr}")
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
if !fix_enabled {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.{attr}")
} else {
format!("{binding}({arg_code}).{attr}")
};
let edit = Edit::range_replacement(replacement, range);
let fix = match applicability {
Some(Applicability::Unsafe) => Fix::unsafe_edits(edit, [import_edit]),
_ => {
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
Fix::applicable_edits(edit, [import_edit], applicability)
}
};
Ok(fix)
});
}
pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {

View File

@@ -1,9 +1,11 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_basename_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.basename`.
@@ -34,7 +36,16 @@ use ruff_python_ast::ExprCall;
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
/// This rule's fix is always marked as unsafe because the replacement is not always semantically
/// equivalent to the original code. In particular, `pathlib` performs path normalization,
/// which can alter the result compared to `os.path.basename`. For example, this normalization:
///
/// - Collapses consecutive slashes (e.g., `"a//b"` → `"a/b"`).
/// - Removes trailing slashes (e.g., `"a/b/"` → `"a/b"`).
/// - Eliminates `"."` (e.g., `"a/./b"` → `"a/b"`).
///
/// As a result, code relying on the exact string returned by `os.path.basename`
/// may behave differently after the fix.
///
/// ## References
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
@@ -62,6 +73,7 @@ pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&
if segments != ["os", "path", "basename"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -69,5 +81,6 @@ pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&
"p",
is_fix_os_path_basename_enabled(checker.settings()),
OsPathBasename,
Some(Applicability::Unsafe),
);
}

View File

@@ -1,9 +1,11 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_dirname_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.dirname`.
@@ -29,7 +31,16 @@ use ruff_python_ast::ExprCall;
/// ```
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
/// This rule's fix is always marked as unsafe because the replacement is not always semantically
/// equivalent to the original code. In particular, `pathlib` performs path normalization,
/// which can alter the result compared to `os.path.dirname`. For example, this normalization:
///
/// - Collapses consecutive slashes (e.g., `"a//b"` → `"a/b"`).
/// - Removes trailing slashes (e.g., `"a/b/"` → `"a/b"`).
/// - Eliminates `"."` (e.g., `"a/./b"` → `"a/b"`).
///
/// As a result, code relying on the exact string returned by `os.path.dirname`
/// may behave differently after the fix.
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
@@ -62,6 +73,7 @@ pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&s
if segments != ["os", "path", "dirname"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -69,5 +81,6 @@ pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&s
"p",
is_fix_os_path_dirname_enabled(checker.settings()),
OsPathDirname,
Some(Applicability::Unsafe),
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_exists_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.exists`.
@@ -62,6 +63,7 @@ pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&st
if segments != ["os", "path", "exists"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -69,5 +71,6 @@ pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_exists_enabled(checker.settings()),
OsPathExists,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_expanduser_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.expanduser`.
@@ -62,6 +63,7 @@ pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &
if segments != ["os", "path", "expanduser"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -69,5 +71,6 @@ pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &
"path",
is_fix_os_path_expanduser_enabled(checker.settings()),
OsPathExpanduser,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getatime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getatime`.
@@ -65,6 +66,7 @@ pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&
if segments != ["os", "path", "getatime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -72,5 +74,6 @@ pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getatime_enabled(checker.settings()),
OsPathGetatime,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getctime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getctime`.
@@ -66,6 +67,7 @@ pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&
if segments != ["os", "path", "getctime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -73,5 +75,6 @@ pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getctime_enabled(checker.settings()),
OsPathGetctime,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getmtime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getmtime`.
@@ -66,6 +67,7 @@ pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&
if segments != ["os", "path", "getmtime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -73,5 +75,6 @@ pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getmtime_enabled(checker.settings()),
OsPathGetmtime,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getsize_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getsize`.
@@ -66,6 +67,7 @@ pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&s
if segments != ["os", "path", "getsize"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -73,5 +75,6 @@ pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&s
"filename",
is_fix_os_path_getsize_enabled(checker.settings()),
OsPathGetsize,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isabs_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isabs`.
@@ -61,6 +62,7 @@ pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str
if segments != ["os", "path", "isabs"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -68,5 +70,6 @@ pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str
"s",
is_fix_os_path_isabs_enabled(checker.settings()),
OsPathIsabs,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isdir`.
@@ -71,5 +72,6 @@ pub(crate) fn os_path_isdir(checker: &Checker, call: &ExprCall, segments: &[&str
"s",
is_fix_os_path_isdir_enabled(checker.settings()),
OsPathIsdir,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isfile_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isfile`.
@@ -71,5 +72,6 @@ pub(crate) fn os_path_isfile(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_isfile_enabled(checker.settings()),
OsPathIsfile,
None,
);
}

View File

@@ -1,9 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_islink_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.islink`.
@@ -71,5 +72,6 @@ pub(crate) fn os_path_islink(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_islink_enabled(checker.settings()),
OsPathIslink,
None,
);
}

View File

@@ -1,11 +1,12 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ExprCall, PythonVersion};
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_readlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ExprCall, PythonVersion};
/// ## What it does
/// Checks for uses of `os.readlink`.
@@ -87,5 +88,6 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
"path",
is_fix_os_readlink_enabled(checker.settings()),
OsReadlink,
None,
);
}

View File

@@ -1,11 +1,12 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_remove_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.remove`.
@@ -82,5 +83,6 @@ pub(crate) fn os_remove(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_remove_enabled(checker.settings()),
OsRemove,
None,
);
}

View File

@@ -1,11 +1,12 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rmdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.rmdir`.
@@ -82,5 +83,6 @@ pub(crate) fn os_rmdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_rmdir_enabled(checker.settings()),
OsRmdir,
None,
);
}

View File

@@ -1,11 +1,12 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_unlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.unlink`.
@@ -82,5 +83,6 @@ pub(crate) fn os_unlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_unlink_enabled(checker.settings()),
OsUnlink,
None,
);
}

View File

@@ -466,6 +466,7 @@ help: Replace with `Path(...).name`
30 | os.path.dirname(p)
31 | os.path.samefile(p)
32 | os.path.splitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
--> full_name.py:29:1
@@ -493,6 +494,7 @@ help: Replace with `Path(...).parent`
31 | os.path.samefile(p)
32 | os.path.splitext(p)
33 | with open(p) as fp:
note: This is an unsafe fix and may change runtime behavior
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> full_name.py:30:1

View File

@@ -466,6 +466,7 @@ help: Replace with `Path(...).name`
30 | foo_p.dirname(p)
31 | foo_p.samefile(p)
32 | foo_p.splitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
--> import_as.py:29:1
@@ -492,6 +493,7 @@ help: Replace with `Path(...).parent`
30 + pathlib.Path(p).parent
31 | foo_p.samefile(p)
32 | foo_p.splitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> import_as.py:30:1

View File

@@ -480,6 +480,7 @@ help: Replace with `Path(...).name`
32 | dirname(p)
33 | samefile(p)
34 | splitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
--> import_from.py:31:1
@@ -508,6 +509,7 @@ help: Replace with `Path(...).parent`
33 | samefile(p)
34 | splitext(p)
35 | with open(p) as fp:
note: This is an unsafe fix and may change runtime behavior
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> import_from.py:32:1

View File

@@ -480,6 +480,7 @@ help: Replace with `Path(...).name`
37 | xdirname(p)
38 | xsamefile(p)
39 | xsplitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
--> import_from_as.py:36:1
@@ -507,6 +508,7 @@ help: Replace with `Path(...).parent`
37 + pathlib.Path(p).parent
38 | xsamefile(p)
39 | xsplitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> import_from_as.py:37:1

View File

@@ -141,7 +141,6 @@ impl Violation for InvalidFirstArgumentNameForClassMethod {
#[derive_message_formats]
// The first string below is what shows up in the documentation
// in the rule table, and it is the more common case.
#[expect(clippy::if_not_else)]
fn message(&self) -> String {
if !self.is_new {
"First argument of a class method should be named `cls`".to_string()

View File

@@ -18,8 +18,8 @@ use crate::rules::pep8_naming::helpers;
/// > (Lets hope that these variables are meant for use inside one module
/// > only.) The conventions are about the same as those for functions.
/// >
/// > Modules that are designed for use via from M import * should use the
/// > __all__ mechanism to prevent exporting globals, or use the older
/// > Modules that are designed for use via `from M import *` should use the
/// > `__all__` mechanism to prevent exporting globals, or use the older
/// > convention of prefixing such globals with an underscore (which you might
/// > want to do to indicate these globals are “module non-public”).
/// >

View File

@@ -47,7 +47,6 @@ impl Violation for UselessImportAlias {
#[derive_message_formats]
fn message(&self) -> String {
#[expect(clippy::if_not_else)]
if !self.required_import_conflict {
"Import alias does not rename original package".to_string()
} else {

View File

@@ -1,10 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast};
use ruff_python_semantic::ScopeKind;
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `yield from` in async functions.
@@ -36,13 +32,3 @@ impl Violation for YieldFromInAsyncFunction {
"`yield from` statement in async function; use `async for` instead".to_string()
}
}
/// PLE1700
pub(crate) fn yield_from_in_async_function(checker: &Checker, expr: &ast::ExprYieldFrom) {
if matches!(
checker.semantic().current_scope().kind,
ScopeKind::Function(ast::StmtFunctionDef { is_async: true, .. })
) {
checker.report_diagnostic(YieldFromInAsyncFunction, expr.range());
}
}

View File

@@ -357,4 +357,19 @@ mod tests {
2 | from pipes import quote, Template
");
}
#[test]
fn unnecessary_default_type_args_stubs_py312_preview() -> Result<()> {
let snapshot = format!("{}__preview", "UP043.pyi");
let diagnostics = test_path(
Path::new("pyupgrade/UP043.pyi"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
unresolved_target_version: PythonVersion::PY312.into(),
..settings::LinterSettings::for_rule(Rule::UnnecessaryDefaultTypeArgs)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -164,7 +164,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
stack.push(&mut string.left);
stack.push(&mut string.right);
}
libcst_native::String::Formatted(_) => {}
libcst_native::String::Formatted(_) | libcst_native::String::Templated(_) => {}
}
}
}

View File

@@ -1,6 +1,8 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
@@ -94,14 +96,22 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
};
// Find the enclosing function definition (if any).
let Some(Stmt::FunctionDef(ast::StmtFunctionDef {
parameters: parent_parameters,
..
})) = parents.find(|stmt| stmt.is_function_def_stmt())
let Some(
func_stmt @ Stmt::FunctionDef(ast::StmtFunctionDef {
parameters: parent_parameters,
..
}),
) = parents.find(|stmt| stmt.is_function_def_stmt())
else {
return;
};
if is_builtins_super(checker.semantic(), call)
&& !has_local_dunder_class_var_ref(checker.semantic(), func_stmt)
{
return;
}
// Extract the name of the first argument to the enclosing function.
let Some(parent_arg) = parent_parameters.args.first() else {
return;
@@ -193,3 +203,67 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
fn is_super_call_with_arguments(call: &ast::ExprCall, checker: &Checker) -> bool {
checker.semantic().match_builtin_expr(&call.func, "super") && !call.arguments.is_empty()
}
/// Returns `true` if the function contains load references to `__class__` or `super` without
/// local binding.
///
/// This indicates that the function relies on the implicit `__class__` cell variable created by
/// Python when `super()` is called without arguments, making it unsafe to remove `super()` parameters.
fn has_local_dunder_class_var_ref(semantic: &SemanticModel, func_stmt: &Stmt) -> bool {
if semantic.current_scope().has("__class__") {
return false;
}
let mut finder = ClassCellReferenceFinder::new();
finder.visit_stmt(func_stmt);
finder.found()
}
/// Returns `true` if the call is to the built-in `builtins.super` function.
fn is_builtins_super(semantic: &SemanticModel, call: &ast::ExprCall) -> bool {
semantic
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["builtins", "super"]))
}
/// A [`Visitor`] that searches for implicit reference to `__class__` cell,
/// excluding nested class definitions.
#[derive(Debug)]
struct ClassCellReferenceFinder {
has_class_cell: bool,
}
impl ClassCellReferenceFinder {
pub(crate) fn new() -> Self {
ClassCellReferenceFinder {
has_class_cell: false,
}
}
pub(crate) fn found(&self) -> bool {
self.has_class_cell
}
}
impl<'a> Visitor<'a> for ClassCellReferenceFinder {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::ClassDef(_) => {}
_ => {
if !self.has_class_cell {
walk_stmt(self, stmt);
}
}
}
}
fn visit_expr(&mut self, expr: &'a Expr) {
if expr.as_name_expr().is_some_and(|name| {
matches!(name.id.as_str(), "super" | "__class__") && name.ctx.is_load()
}) {
self.has_class_cell = true;
return;
}
walk_expr(self, expr);
}
}

View File

@@ -8,6 +8,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// ## What it does
/// Checks for unnecessary default type arguments for `Generator` and
/// `AsyncGenerator` on Python 3.13+.
/// In [preview], this rule will also apply to stub files.
///
/// ## Why is this bad?
/// Python 3.13 introduced the ability for type parameters to specify default
@@ -59,6 +60,8 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// - [Annotating generators and coroutines](https://docs.python.org/3/library/typing.html#annotating-generators-and-coroutines)
/// - [Python documentation: `typing.Generator`](https://docs.python.org/3/library/typing.html#typing.Generator)
/// - [Python documentation: `typing.AsyncGenerator`](https://docs.python.org/3/library/typing.html#typing.AsyncGenerator)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct UnnecessaryDefaultTypeArgs;

View File

@@ -146,25 +146,6 @@ help: Remove `super()` parameters
95 | # see: https://github.com/astral-sh/ruff/issues/18684
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:107:23
|
105 | class C:
106 | def f(self):
107 | builtins.super(C, self)
| ^^^^^^^^^
|
help: Remove `super()` parameters
104 |
105 | class C:
106 | def f(self):
- builtins.super(C, self)
107 + builtins.super()
108 |
109 |
110 | # see: https://github.com/astral-sh/ruff/issues/18533
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:113:14
|
@@ -294,6 +275,8 @@ UP008 [*] Use `super()` instead of `super(__class__, self)`
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
help: Remove `super()` parameters
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
@@ -301,4 +284,213 @@ help: Remove `super()` parameters
142 | def method3(self):
- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 + super().some_method() # Should be fixed - no keywords
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
146 | # Must be detected
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:154:23
|
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
154 | builtins.super(ChildD1, self).f()
| ^^^^^^^^^^^^^^^
155 |
156 | class ChildD2(ParentD):
|
help: Remove `super()` parameters
151 | class ChildD1(ParentD):
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
- builtins.super(ChildD1, self).f()
154 + builtins.super().f()
155 |
156 | class ChildD2(ParentD):
157 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:159:23
|
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
159 | builtins.super(ChildD2, self).f()
| ^^^^^^^^^^^^^^^
160 |
161 | class ChildD3(ParentD):
|
help: Remove `super()` parameters
156 | class ChildD2(ParentD):
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
- builtins.super(ChildD2, self).f()
159 + builtins.super().f()
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:163:23
|
161 | class ChildD3(ParentD):
162 | def f(self):
163 | builtins.super(ChildD3, self).f()
| ^^^^^^^^^^^^^^^
164 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
- builtins.super(ChildD3, self).f()
163 + builtins.super().f()
164 | super # Python injects __class__ into scope
165 |
166 | import builtins as builtins_alias
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:169:29
|
167 | class ChildD4(ParentD):
168 | def f(self):
169 | builtins_alias.super(ChildD4, self).f()
| ^^^^^^^^^^^^^^^
170 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
166 | import builtins as builtins_alias
167 | class ChildD4(ParentD):
168 | def f(self):
- builtins_alias.super(ChildD4, self).f()
169 + builtins_alias.super().f()
170 | super # Python injects __class__ into scope
171 |
172 | class ChildD5(ParentD):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:176:23
|
174 | super = 1
175 | super # Python injects __class__ into scope
176 | builtins.super(ChildD5, self).f()
| ^^^^^^^^^^^^^^^
177 |
178 | class ChildD6(ParentD):
|
help: Remove `super()` parameters
173 | def f(self):
174 | super = 1
175 | super # Python injects __class__ into scope
- builtins.super(ChildD5, self).f()
176 + builtins.super().f()
177 |
178 | class ChildD6(ParentD):
179 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:182:23
|
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
182 | builtins.super(ChildD6, self).f()
| ^^^^^^^^^^^^^^^
183 |
184 | class ChildD7(ParentD):
|
help: Remove `super()` parameters
179 | def f(self):
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD6, self).f()
182 + builtins.super().f()
183 |
184 | class ChildD7(ParentD):
185 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:188:23
|
186 | def x():
187 | __class__ # Python injects __class__ into scope
188 | builtins.super(ChildD7, self).f()
| ^^^^^^^^^^^^^^^
189 |
190 | class ChildD8(ParentD):
|
help: Remove `super()` parameters
185 | def f(self):
186 | def x():
187 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD7, self).f()
188 + builtins.super().f()
189 |
190 | class ChildD8(ParentD):
191 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:195:23
|
193 | super = 1
194 | super # Python injects __class__ into scope
195 | builtins.super(ChildD8, self).f()
| ^^^^^^^^^^^^^^^
196 |
197 | class ChildD9(ParentD):
|
help: Remove `super()` parameters
192 | def x():
193 | super = 1
194 | super # Python injects __class__ into scope
- builtins.super(ChildD8, self).f()
195 + builtins.super().f()
196 |
197 | class ChildD9(ParentD):
198 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:202:23
|
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
202 | builtins.super(ChildD9, self).f()
| ^^^^^^^^^^^^^^^
203 |
204 | class ChildD10(ParentD):
|
help: Remove `super()` parameters
199 | def x():
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD9, self).f()
202 + builtins.super().f()
203 |
204 | class ChildD10(ParentD):
205 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:209:23
|
207 | __class__ = 1
208 | super # Python injects __class__ into scope
209 | builtins.super(ChildD10, self).f()
| ^^^^^^^^^^^^^^^^
|
help: Remove `super()` parameters
206 | def x():
207 | __class__ = 1
208 | super # Python injects __class__ into scope
- builtins.super(ChildD10, self).f()
209 + builtins.super().f()
210 |
211 |
212 | # Must be ignored
note: This is an unsafe fix and may change runtime behavior

View File

@@ -139,24 +139,6 @@ help: Remove `super()` parameters
94 |
95 | # see: https://github.com/astral-sh/ruff/issues/18684
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:107:23
|
105 | class C:
106 | def f(self):
107 | builtins.super(C, self)
| ^^^^^^^^^
|
help: Remove `super()` parameters
104 |
105 | class C:
106 | def f(self):
- builtins.super(C, self)
107 + builtins.super()
108 |
109 |
110 | # see: https://github.com/astral-sh/ruff/issues/18533
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:113:14
|
@@ -286,6 +268,8 @@ UP008 [*] Use `super()` instead of `super(__class__, self)`
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
help: Remove `super()` parameters
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
@@ -293,3 +277,202 @@ help: Remove `super()` parameters
142 | def method3(self):
- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 + super().some_method() # Should be fixed - no keywords
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
146 | # Must be detected
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:154:23
|
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
154 | builtins.super(ChildD1, self).f()
| ^^^^^^^^^^^^^^^
155 |
156 | class ChildD2(ParentD):
|
help: Remove `super()` parameters
151 | class ChildD1(ParentD):
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
- builtins.super(ChildD1, self).f()
154 + builtins.super().f()
155 |
156 | class ChildD2(ParentD):
157 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:159:23
|
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
159 | builtins.super(ChildD2, self).f()
| ^^^^^^^^^^^^^^^
160 |
161 | class ChildD3(ParentD):
|
help: Remove `super()` parameters
156 | class ChildD2(ParentD):
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
- builtins.super(ChildD2, self).f()
159 + builtins.super().f()
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:163:23
|
161 | class ChildD3(ParentD):
162 | def f(self):
163 | builtins.super(ChildD3, self).f()
| ^^^^^^^^^^^^^^^
164 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
- builtins.super(ChildD3, self).f()
163 + builtins.super().f()
164 | super # Python injects __class__ into scope
165 |
166 | import builtins as builtins_alias
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:169:29
|
167 | class ChildD4(ParentD):
168 | def f(self):
169 | builtins_alias.super(ChildD4, self).f()
| ^^^^^^^^^^^^^^^
170 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
166 | import builtins as builtins_alias
167 | class ChildD4(ParentD):
168 | def f(self):
- builtins_alias.super(ChildD4, self).f()
169 + builtins_alias.super().f()
170 | super # Python injects __class__ into scope
171 |
172 | class ChildD5(ParentD):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:176:23
|
174 | super = 1
175 | super # Python injects __class__ into scope
176 | builtins.super(ChildD5, self).f()
| ^^^^^^^^^^^^^^^
177 |
178 | class ChildD6(ParentD):
|
help: Remove `super()` parameters
173 | def f(self):
174 | super = 1
175 | super # Python injects __class__ into scope
- builtins.super(ChildD5, self).f()
176 + builtins.super().f()
177 |
178 | class ChildD6(ParentD):
179 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:182:23
|
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
182 | builtins.super(ChildD6, self).f()
| ^^^^^^^^^^^^^^^
183 |
184 | class ChildD7(ParentD):
|
help: Remove `super()` parameters
179 | def f(self):
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD6, self).f()
182 + builtins.super().f()
183 |
184 | class ChildD7(ParentD):
185 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:188:23
|
186 | def x():
187 | __class__ # Python injects __class__ into scope
188 | builtins.super(ChildD7, self).f()
| ^^^^^^^^^^^^^^^
189 |
190 | class ChildD8(ParentD):
|
help: Remove `super()` parameters
185 | def f(self):
186 | def x():
187 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD7, self).f()
188 + builtins.super().f()
189 |
190 | class ChildD8(ParentD):
191 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:195:23
|
193 | super = 1
194 | super # Python injects __class__ into scope
195 | builtins.super(ChildD8, self).f()
| ^^^^^^^^^^^^^^^
196 |
197 | class ChildD9(ParentD):
|
help: Remove `super()` parameters
192 | def x():
193 | super = 1
194 | super # Python injects __class__ into scope
- builtins.super(ChildD8, self).f()
195 + builtins.super().f()
196 |
197 | class ChildD9(ParentD):
198 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:202:23
|
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
202 | builtins.super(ChildD9, self).f()
| ^^^^^^^^^^^^^^^
203 |
204 | class ChildD10(ParentD):
|
help: Remove `super()` parameters
199 | def x():
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD9, self).f()
202 + builtins.super().f()
203 |
204 | class ChildD10(ParentD):
205 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:209:23
|
207 | __class__ = 1
208 | super # Python injects __class__ into scope
209 | builtins.super(ChildD10, self).f()
| ^^^^^^^^^^^^^^^^
|
help: Remove `super()` parameters
206 | def x():
207 | __class__ = 1
208 | super # Python injects __class__ into scope
- builtins.super(ChildD10, self).f()
209 + builtins.super().f()
210 |
211 |
212 | # Must be ignored

View File

@@ -0,0 +1,128 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:4:15
|
4 | def func() -> Generator[int, None, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
5 | yield 42
|
help: Remove default type arguments
1 | from collections.abc import Generator, AsyncGenerator
2 |
3 |
- def func() -> Generator[int, None, None]:
4 + def func() -> Generator[int]:
5 | yield 42
6 |
7 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:8:15
|
8 | def func() -> Generator[int, None]:
| ^^^^^^^^^^^^^^^^^^^^
9 | yield 42
|
help: Remove default type arguments
5 | yield 42
6 |
7 |
- def func() -> Generator[int, None]:
8 + def func() -> Generator[int]:
9 | yield 42
10 |
11 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:21:15
|
21 | def func() -> Generator[int, int, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^
22 | _ = yield 42
23 | return None
|
help: Remove default type arguments
18 | return foo
19 |
20 |
- def func() -> Generator[int, int, None]:
21 + def func() -> Generator[int, int]:
22 | _ = yield 42
23 | return None
24 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:31:21
|
31 | async def func() -> AsyncGenerator[int, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^
32 | yield 42
|
help: Remove default type arguments
28 | return 42
29 |
30 |
- async def func() -> AsyncGenerator[int, None]:
31 + async def func() -> AsyncGenerator[int]:
32 | yield 42
33 |
34 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:47:15
|
47 | def func() -> Generator[str, None, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
48 | yield "hello"
|
help: Remove default type arguments
44 | from typing import Generator, AsyncGenerator
45 |
46 |
- def func() -> Generator[str, None, None]:
47 + def func() -> Generator[str]:
48 | yield "hello"
49 |
50 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:51:21
|
51 | async def func() -> AsyncGenerator[str, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^
52 | yield "hello"
|
help: Remove default type arguments
48 | yield "hello"
49 |
50 |
- async def func() -> AsyncGenerator[str, None]:
51 + async def func() -> AsyncGenerator[str]:
52 | yield "hello"
53 |
54 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:55:21
|
55 | async def func() -> AsyncGenerator[ # type: ignore
| _____________________^
56 | | str,
57 | | None
58 | | ]:
| |_^
59 | yield "hello"
|
help: Remove default type arguments
52 | yield "hello"
53 |
54 |
- async def func() -> AsyncGenerator[ # type: ignore
- str,
- None
- ]:
55 + async def func() -> AsyncGenerator[str]:
56 | yield "hello"
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_python_ast::{self as ast, CmpOp, Expr, helpers::is_empty_f_string};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -75,10 +75,7 @@ fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool {
Expr::Dict(ast::ExprDict { items, .. }) => items.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
Expr::FString(s) => s
.value
.elements()
.all(|elt| elt.as_literal().is_some_and(|elt| elt.is_empty())),
Expr::FString(s) => is_empty_f_string(s),
Expr::Call(ast::ExprCall {
func,
arguments,

View File

@@ -251,3 +251,12 @@ RUF060 Unnecessary membership test on empty collection
25 |
26 | # OK
|
RUF060 Unnecessary membership test on empty collection
--> RUF060.py:47:1
|
46 | # https://github.com/astral-sh/ruff/issues/20238
47 | "b" in f"" "" # Error
| ^^^^^^^^^^^^^
48 | "b" in f"" "x" # OK
|

View File

@@ -0,0 +1,9 @@
---
source: crates/ruff_linter/src/linter.rs
---
PLE1700 `yield from` statement in async function; use `async for` instead
--> resources/test/fixtures/syntax_errors/yield_from_in_async_function.py:1:16
|
1 | async def f(): yield from x # error
| ^^^^^^^^^^^^
|

View File

@@ -3030,6 +3030,12 @@ impl Parameters {
.find(|arg| arg.parameter.name.as_str() == name)
}
/// Returns the index of the parameter with the given name
pub fn index(&self, name: &str) -> Option<usize> {
self.iter_non_variadic_params()
.position(|arg| arg.parameter.name.as_str() == name)
}
/// Returns an iterator over all parameters included in this [`Parameters`] node.
pub fn iter(&self) -> ParametersIterator<'_> {
ParametersIterator::new(self)
@@ -3219,7 +3225,6 @@ impl<'a> IntoIterator for &'a Box<Parameters> {
/// Used by `Arguments` original type.
///
/// NOTE: This type is different from original Python AST.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct ParameterWithDefault {
@@ -3241,6 +3246,14 @@ impl ParameterWithDefault {
pub fn annotation(&self) -> Option<&Expr> {
self.parameter.annotation()
}
/// Return `true` if the parameter name uses the pre-PEP-570 convention
/// (specified in PEP 484) to indicate to a type checker that it should be treated
/// as positional-only.
pub fn uses_pep_484_positional_only_convention(&self) -> bool {
let name = self.name();
name.starts_with("__") && !name.ends_with("__")
}
}
/// An AST node used to represent the arguments passed to a function call or class definition.

View File

@@ -1,6 +1,6 @@
pub use generator::Generator;
use ruff_python_parser::{ParseError, parse_module};
pub use stylist::Stylist;
pub use stylist::{Indentation, Stylist};
mod generator;
mod stylist;

View File

@@ -252,15 +252,20 @@ impl QuoteStyle {
pub const fn is_preserve(self) -> bool {
matches!(self, QuoteStyle::Preserve)
}
/// Returns the string representation of the quote style.
pub const fn as_str(&self) -> &'static str {
match self {
QuoteStyle::Single => "single",
QuoteStyle::Double => "double",
QuoteStyle::Preserve => "preserve",
}
}
}
impl fmt::Display for QuoteStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Single => write!(f, "single"),
Self::Double => write!(f, "double"),
Self::Preserve => write!(f, "preserve"),
}
f.write_str(self.as_str())
}
}
@@ -302,10 +307,10 @@ impl MagicTrailingComma {
impl fmt::Display for MagicTrailingComma {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Respect => write!(f, "respect"),
Self::Ignore => write!(f, "ignore"),
}
f.write_str(match self {
MagicTrailingComma::Respect => "respect",
MagicTrailingComma::Ignore => "ignore",
})
}
}

View File

@@ -0,0 +1 @@
async def f(): yield from x

View File

@@ -709,6 +709,16 @@ impl SemanticSyntaxChecker {
}
Expr::YieldFrom(_) => {
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::YieldFrom);
if ctx.in_function_scope() && ctx.in_async_context() {
// test_err yield_from_in_async_function
// async def f(): yield from x
Self::add_error(
ctx,
SemanticSyntaxErrorKind::YieldFromInAsyncFunction,
expr.range(),
);
}
}
Expr::Await(_) => {
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
@@ -989,6 +999,9 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
write!(f, "annotated name `{name}` can't be nonlocal")
}
SemanticSyntaxErrorKind::YieldFromInAsyncFunction => {
f.write_str("`yield from` statement in async function; use `async for` instead")
}
}
}
}
@@ -1346,6 +1359,9 @@ pub enum SemanticSyntaxErrorKind {
/// Represents a type annotation on a variable that's been declared nonlocal
AnnotatedNonlocal(String),
/// Represents the use of `yield from` inside an asynchronous function.
YieldFromInAsyncFunction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]

View File

@@ -465,7 +465,7 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
enum Scope {
Module,
Function,
Function { is_async: bool },
Comprehension { is_async: bool },
Class,
}
@@ -528,7 +528,15 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
}
fn in_async_context(&self) -> bool {
true
if let Some(scope) = self.scopes.iter().next_back() {
match scope {
Scope::Class | Scope::Module => false,
Scope::Comprehension { is_async } => *is_async,
Scope::Function { is_async } => *is_async,
}
} else {
false
}
}
fn in_sync_comprehension(&self) -> bool {
@@ -589,8 +597,10 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> {
self.visit_body(body);
self.scopes.pop().unwrap();
}
ast::Stmt::FunctionDef(ast::StmtFunctionDef { .. }) => {
self.scopes.push(Scope::Function);
ast::Stmt::FunctionDef(ast::StmtFunctionDef { is_async, .. }) => {
self.scopes.push(Scope::Function {
is_async: *is_async,
});
ast::visitor::walk_stmt(self, stmt);
self.scopes.pop().unwrap();
}
@@ -604,7 +614,7 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> {
self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context));
match expr {
ast::Expr::Lambda(_) => {
self.scopes.push(Scope::Function);
self.scopes.push(Scope::Function { is_async: false });
ast::visitor::walk_expr(self, expr);
self.scopes.pop().unwrap();
}

View File

@@ -0,0 +1,68 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/yield_from_in_async_function.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..28,
body: [
FunctionDef(
StmtFunctionDef {
node_index: NodeIndex(None),
range: 0..27,
is_async: true,
decorator_list: [],
name: Identifier {
id: Name("f"),
range: 10..11,
node_index: NodeIndex(None),
},
type_params: None,
parameters: Parameters {
range: 11..13,
node_index: NodeIndex(None),
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 15..27,
value: YieldFrom(
ExprYieldFrom {
node_index: NodeIndex(None),
range: 15..27,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 26..27,
id: Name("x"),
ctx: Load,
},
),
},
),
},
),
],
},
),
],
},
)
```
## Semantic Syntax Errors
|
1 | async def f(): yield from x
| ^^^^^^^^^^^^ Syntax Error: `yield from` statement in async function; use `async for` instead
|

View File

@@ -50,5 +50,8 @@ insta = { workspace = true }
[target.'cfg(target_vendor = "apple")'.dependencies]
libc = { workspace = true }
[features]
test-uv = []
[lints]
workspace = true

View File

@@ -1,18 +1,52 @@
use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
use ruff_formatter::PrintedRange;
use anyhow::Context;
use ruff_formatter::{FormatOptions, PrintedRange};
use ruff_python_ast::PySourceType;
use ruff_python_formatter::{FormatModuleError, format_module_source};
use ruff_python_formatter::{FormatModuleError, PyFormatOptions, format_module_source};
use ruff_source_file::LineIndex;
use ruff_text_size::TextRange;
use ruff_workspace::FormatterSettings;
use crate::edit::TextDocument;
/// The backend to use for formatting.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum FormatBackend {
/// Use the built-in Ruff formatter.
///
/// The formatter version will match the LSP version.
#[default]
Internal,
/// Use uv for formatting.
///
/// The formatter version may differ from the LSP version.
Uv,
}
pub(crate) fn format(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
path: &Path,
backend: FormatBackend,
) -> crate::Result<Option<String>> {
match backend {
FormatBackend::Uv => format_external(document, source_type, formatter_settings, path),
FormatBackend::Internal => format_internal(document, source_type, formatter_settings, path),
}
}
/// Format using the built-in Ruff formatter.
fn format_internal(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
path: &Path,
) -> crate::Result<Option<String>> {
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
@@ -35,12 +69,44 @@ pub(crate) fn format(
}
}
/// Format using an external uv command.
fn format_external(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
path: &Path,
) -> crate::Result<Option<String>> {
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
let uv_command = UvFormatCommand::from(format_options);
uv_command.format_document(document.contents(), path)
}
pub(crate) fn format_range(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: &Path,
backend: FormatBackend,
) -> crate::Result<Option<PrintedRange>> {
match backend {
FormatBackend::Uv => {
format_range_external(document, source_type, formatter_settings, range, path)
}
FormatBackend::Internal => {
format_range_internal(document, source_type, formatter_settings, range, path)
}
}
}
/// Format range using the built-in Ruff formatter
fn format_range_internal(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: &Path,
) -> crate::Result<Option<PrintedRange>> {
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
@@ -63,6 +129,198 @@ pub(crate) fn format_range(
}
}
/// Format range using an external command, i.e., `uv`.
fn format_range_external(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: &Path,
) -> crate::Result<Option<PrintedRange>> {
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
let uv_command = UvFormatCommand::from(format_options);
// Format the range using uv and convert the result to `PrintedRange`
match uv_command.format_range(document.contents(), range, path, document.index())? {
Some(formatted) => Ok(Some(PrintedRange::new(formatted, range))),
None => Ok(None),
}
}
/// Builder for uv format commands
#[derive(Debug)]
pub(crate) struct UvFormatCommand {
options: PyFormatOptions,
}
impl From<PyFormatOptions> for UvFormatCommand {
fn from(options: PyFormatOptions) -> Self {
Self { options }
}
}
impl UvFormatCommand {
/// Build the command with all necessary arguments
fn build_command(
&self,
path: &Path,
range_with_index: Option<(TextRange, &LineIndex, &str)>,
) -> Command {
let mut command = Command::new("uv");
command.arg("format");
command.arg("--");
let target_version = format!(
"py{}{}",
self.options.target_version().major,
self.options.target_version().minor
);
// Add only the formatting options that the CLI supports
command.arg("--target-version");
command.arg(&target_version);
command.arg("--line-length");
command.arg(self.options.line_width().to_string());
if self.options.preview().is_enabled() {
command.arg("--preview");
}
// Pass other formatting options via --config
command.arg("--config");
command.arg(format!(
"format.indent-style = '{}'",
self.options.indent_style()
));
command.arg("--config");
command.arg(format!("indent-width = {}", self.options.indent_width()));
command.arg("--config");
command.arg(format!(
"format.quote-style = '{}'",
self.options.quote_style()
));
command.arg("--config");
command.arg(format!(
"format.line-ending = '{}'",
self.options.line_ending().as_setting_str()
));
command.arg("--config");
command.arg(format!(
"format.skip-magic-trailing-comma = {}",
match self.options.magic_trailing_comma() {
ruff_python_formatter::MagicTrailingComma::Respect => "false",
ruff_python_formatter::MagicTrailingComma::Ignore => "true",
}
));
if let Some((range, line_index, source)) = range_with_index {
// The CLI expects line:column format
let start_pos = line_index.line_column(range.start(), source);
let end_pos = line_index.line_column(range.end(), source);
let range_str = format!(
"{}:{}-{}:{}",
start_pos.line.get(),
start_pos.column.get(),
end_pos.line.get(),
end_pos.column.get()
);
command.arg("--range");
command.arg(&range_str);
}
command.arg("--stdin-filename");
command.arg(path.to_string_lossy().as_ref());
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
command
}
/// Execute the format command on the given source.
pub(crate) fn format(
&self,
source: &str,
path: &Path,
range_with_index: Option<(TextRange, &LineIndex)>,
) -> crate::Result<Option<String>> {
let mut command =
self.build_command(path, range_with_index.map(|(r, idx)| (r, idx, source)));
let mut child = match command.spawn() {
Ok(child) => child,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
anyhow::bail!("uv was not found; is it installed and on the PATH?")
}
Err(err) => return Err(err).context("Failed to spawn uv"),
};
let mut stdin = child
.stdin
.take()
.context("Failed to get stdin from format subprocess")?;
stdin
.write_all(source.as_bytes())
.context("Failed to write to stdin")?;
drop(stdin);
let result = child
.wait_with_output()
.context("Failed to get output from format subprocess")?;
if !result.status.success() {
let stderr = String::from_utf8_lossy(&result.stderr);
// We don't propagate format errors due to invalid syntax
if stderr.contains("Failed to parse") {
tracing::warn!("Unable to format document: {}", stderr);
return Ok(None);
}
// Special-case for when `uv format` is not available
if stderr.contains("unrecognized subcommand 'format'") {
anyhow::bail!(
"The installed version of uv does not support `uv format`; upgrade to a newer version"
);
}
anyhow::bail!("Failed to format document: {}", stderr);
}
let formatted = String::from_utf8(result.stdout)
.context("Failed to parse stdout from format subprocess as utf-8")?;
if formatted == source {
Ok(None)
} else {
Ok(Some(formatted))
}
}
/// Format the entire document.
pub(crate) fn format_document(
&self,
source: &str,
path: &Path,
) -> crate::Result<Option<String>> {
self.format(source, path, None)
}
/// Format a specific range.
pub(crate) fn format_range(
&self,
source: &str,
range: TextRange,
path: &Path,
line_index: &LineIndex,
) -> crate::Result<Option<String>> {
self.format(source, path, Some((range, line_index)))
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
@@ -74,7 +332,7 @@ mod tests {
use ruff_workspace::FormatterSettings;
use crate::TextDocument;
use crate::format::{format, format_range};
use crate::format::{FormatBackend, format, format_range};
#[test]
fn format_per_file_version() {
@@ -98,6 +356,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
..Default::default()
},
Path::new("test.py"),
FormatBackend::Internal,
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
@@ -120,6 +379,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
..Default::default()
},
Path::new("test.py"),
FormatBackend::Internal,
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
@@ -168,6 +428,7 @@ sys.exit(
},
range,
Path::new("test.py"),
FormatBackend::Internal,
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
@@ -191,6 +452,7 @@ sys.exit(
},
range,
Path::new("test.py"),
FormatBackend::Internal,
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
@@ -204,4 +466,279 @@ sys.exit(
Ok(())
}
#[cfg(feature = "test-uv")]
mod uv_tests {
use super::*;
#[test]
fn test_uv_format_document() {
let document = TextDocument::new(
r#"
def hello( x,y ,z ):
return x+y +z
def world( ):
pass
"#
.to_string(),
0,
);
let result = format(
&document,
PySourceType::Python,
&FormatterSettings::default(),
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
// uv should format this to a consistent style
assert_snapshot!(result, @r#"
def hello(x, y, z):
return x + y + z
def world():
pass
"#);
}
#[test]
fn test_uv_format_range() -> anyhow::Result<()> {
let document = TextDocument::new(
r#"
def messy_function( a, b,c ):
return a+b+c
def another_function(x,y,z):
result=x+y+z
return result
"#
.to_string(),
0,
);
// Find the range of the second function
let start = document.contents().find("def another_function").unwrap();
let end = document.contents().find("return result").unwrap() + "return result".len();
let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);
let result = format_range(
&document,
PySourceType::Python,
&FormatterSettings::default(),
range,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting range with uv")
.expect("Expected formatting changes");
assert_snapshot!(result.as_code(), @r#"
def messy_function( a, b,c ):
return a+b+c
def another_function(x, y, z):
result = x + y + z
return result
"#);
Ok(())
}
#[test]
fn test_uv_format_with_line_length() {
use ruff_formatter::LineWidth;
let document = TextDocument::new(
r#"
def hello(very_long_parameter_name_1, very_long_parameter_name_2, very_long_parameter_name_3):
return very_long_parameter_name_1 + very_long_parameter_name_2 + very_long_parameter_name_3
"#
.to_string(),
0,
);
// Test with shorter line length
let formatter_settings = FormatterSettings {
line_width: LineWidth::try_from(60).unwrap(),
..Default::default()
};
let result = format(
&document,
PySourceType::Python,
&formatter_settings,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
// With line length 60, the function should be wrapped
assert_snapshot!(result, @r#"
def hello(
very_long_parameter_name_1,
very_long_parameter_name_2,
very_long_parameter_name_3,
):
return (
very_long_parameter_name_1
+ very_long_parameter_name_2
+ very_long_parameter_name_3
)
"#);
}
#[test]
fn test_uv_format_with_indent_style() {
use ruff_formatter::IndentStyle;
let document = TextDocument::new(
r#"
def hello():
if True:
print("Hello")
if False:
print("World")
"#
.to_string(),
0,
);
// Test with tabs instead of spaces
let formatter_settings = FormatterSettings {
indent_style: IndentStyle::Tab,
..Default::default()
};
let result = format(
&document,
PySourceType::Python,
&formatter_settings,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
// Should have formatting changes (spaces to tabs)
assert_snapshot!(result, @r#"
def hello():
if True:
print("Hello")
if False:
print("World")
"#);
}
#[test]
fn test_uv_format_syntax_error() {
let document = TextDocument::new(
r#"
def broken(:
pass
"#
.to_string(),
0,
);
// uv should return None for syntax errors (as indicated by the TODO comment)
let result = format(
&document,
PySourceType::Python,
&FormatterSettings::default(),
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors from format function");
// Should return None since the syntax is invalid
assert_eq!(result, None, "Expected None for syntax error");
}
#[test]
fn test_uv_format_with_quote_style() {
use ruff_python_formatter::QuoteStyle;
let document = TextDocument::new(
r#"
x = "hello"
y = 'world'
z = '''multi
line'''
"#
.to_string(),
0,
);
// Test with single quotes
let formatter_settings = FormatterSettings {
quote_style: QuoteStyle::Single,
..Default::default()
};
let result = format(
&document,
PySourceType::Python,
&formatter_settings,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
assert_snapshot!(result, @r#"
x = 'hello'
y = 'world'
z = """multi
line"""
"#);
}
#[test]
fn test_uv_format_with_magic_trailing_comma() {
use ruff_python_formatter::MagicTrailingComma;
let document = TextDocument::new(
r#"
foo = [
1,
2,
3,
]
bar = [1, 2, 3,]
"#
.to_string(),
0,
);
// Test with ignore magic trailing comma
let formatter_settings = FormatterSettings {
magic_trailing_comma: MagicTrailingComma::Ignore,
..Default::default()
};
let result = format(
&document,
PySourceType::Python,
&formatter_settings,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
assert_snapshot!(result, @r#"
foo = [1, 2, 3]
bar = [1, 2, 3]
"#);
}
}
}

View File

@@ -33,6 +33,10 @@ impl super::BackgroundDocumentRequestHandler for Format {
pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes> {
let mut fixes = Fixes::default();
let query = snapshot.query();
let backend = snapshot
.client_settings()
.editor_settings()
.format_backend();
match snapshot.query() {
DocumentQuery::Notebook { notebook, .. } => {
@@ -41,7 +45,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
.map(|url| (url.clone(), notebook.cell_document_by_uri(url).unwrap()))
{
if let Some(changes) =
format_text_document(text_document, query, snapshot.encoding(), true)?
format_text_document(text_document, query, snapshot.encoding(), true, backend)?
{
fixes.insert(url, changes);
}
@@ -49,7 +53,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
}
DocumentQuery::Text { document, .. } => {
if let Some(changes) =
format_text_document(document, query, snapshot.encoding(), false)?
format_text_document(document, query, snapshot.encoding(), false, backend)?
{
fixes.insert(snapshot.query().make_key().into_url(), changes);
}
@@ -68,11 +72,16 @@ pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result<super::Form
.context("Failed to get text document for the format request")
.unwrap();
let query = snapshot.query();
let backend = snapshot
.client_settings()
.editor_settings()
.format_backend();
format_text_document(
text_document,
query,
snapshot.encoding(),
query.as_notebook().is_some(),
backend,
)
}
@@ -81,6 +90,7 @@ fn format_text_document(
query: &DocumentQuery,
encoding: PositionEncoding,
is_notebook: bool,
backend: crate::format::FormatBackend,
) -> Result<super::FormatResponse> {
let settings = query.settings();
let file_path = query.virtual_file_path();
@@ -101,6 +111,7 @@ fn format_text_document(
query.source_type(),
&settings.formatter,
&file_path,
backend,
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
let Some(mut formatted) = formatted else {

View File

@@ -36,7 +36,11 @@ fn format_document_range(
.context("Failed to get text document for the format range request")
.unwrap();
let query = snapshot.query();
format_text_document_range(text_document, range, query, snapshot.encoding())
let backend = snapshot
.client_settings()
.editor_settings()
.format_backend();
format_text_document_range(text_document, range, query, snapshot.encoding(), backend)
}
/// Formats the specified [`Range`] in the [`TextDocument`].
@@ -45,6 +49,7 @@ fn format_text_document_range(
range: Range,
query: &DocumentQuery,
encoding: PositionEncoding,
backend: crate::format::FormatBackend,
) -> Result<super::FormatResponse> {
let settings = query.settings();
let file_path = query.virtual_file_path();
@@ -68,6 +73,7 @@ fn format_text_document_range(
&settings.formatter,
range,
&file_path,
backend,
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;

View File

@@ -401,6 +401,7 @@ impl ConfigurationTransformer for EditorConfigurationTransformer<'_> {
configuration,
format_preview,
lint_preview,
format_backend: _,
select,
extend_select,
ignore,

View File

@@ -7,9 +7,12 @@ use serde_json::{Map, Value};
use ruff_linter::{RuleSelector, line_width::LineLength, rule_selector::ParseError};
use crate::session::{
Client,
settings::{ClientSettings, EditorSettings, GlobalClientSettings, ResolvedConfiguration},
use crate::{
format::FormatBackend,
session::{
Client,
settings::{ClientSettings, EditorSettings, GlobalClientSettings, ResolvedConfiguration},
},
};
pub(crate) type WorkspaceOptionsMap = FxHashMap<Url, ClientOptions>;
@@ -124,6 +127,7 @@ impl ClientOptions {
configuration,
lint_preview: lint.preview,
format_preview: format.preview,
format_backend: format.backend,
select: lint.select.and_then(|select| {
Self::resolve_rules(
&select,
@@ -283,11 +287,13 @@ impl Combine for LintOptions {
#[serde(rename_all = "camelCase")]
struct FormatOptions {
preview: Option<bool>,
backend: Option<FormatBackend>,
}
impl Combine for FormatOptions {
fn combine_with(&mut self, other: Self) {
self.preview.combine_with(other.preview);
self.backend.combine_with(other.backend);
}
}
@@ -443,6 +449,12 @@ pub(crate) trait Combine {
fn combine_with(&mut self, other: Self);
}
impl Combine for FormatBackend {
fn combine_with(&mut self, other: Self) {
*self = other;
}
}
impl<T> Combine for Option<T>
where
T: Combine,
@@ -584,6 +596,7 @@ mod tests {
format: Some(
FormatOptions {
preview: None,
backend: None,
},
),
code_action: Some(
@@ -640,6 +653,7 @@ mod tests {
format: Some(
FormatOptions {
preview: None,
backend: None,
},
),
code_action: Some(
@@ -704,6 +718,7 @@ mod tests {
format: Some(
FormatOptions {
preview: None,
backend: None,
},
),
code_action: Some(
@@ -782,6 +797,7 @@ mod tests {
configuration: None,
lint_preview: Some(true),
format_preview: None,
format_backend: None,
select: Some(vec![
RuleSelector::Linter(Linter::Pyflakes),
RuleSelector::Linter(Linter::Isort)
@@ -819,6 +835,7 @@ mod tests {
configuration: None,
lint_preview: Some(false),
format_preview: None,
format_backend: None,
select: Some(vec![
RuleSelector::Linter(Linter::Pyflakes),
RuleSelector::Linter(Linter::Isort)
@@ -919,6 +936,7 @@ mod tests {
configuration: None,
lint_preview: None,
format_preview: None,
format_backend: None,
select: None,
extend_select: None,
ignore: Some(vec![RuleSelector::from_str("RUF001").unwrap()]),

View File

@@ -8,6 +8,7 @@ use ruff_workspace::options::Options;
use crate::{
ClientOptions,
format::FormatBackend,
session::{
Client,
options::{ClientConfiguration, ConfigurationPreference},
@@ -84,6 +85,7 @@ pub(crate) struct EditorSettings {
pub(super) configuration: Option<ResolvedConfiguration>,
pub(super) lint_preview: Option<bool>,
pub(super) format_preview: Option<bool>,
pub(super) format_backend: Option<FormatBackend>,
pub(super) select: Option<Vec<RuleSelector>>,
pub(super) extend_select: Option<Vec<RuleSelector>>,
pub(super) ignore: Option<Vec<RuleSelector>>,
@@ -163,3 +165,9 @@ impl ClientSettings {
&self.editor_settings
}
}
impl EditorSettings {
pub(crate) fn format_backend(&self) -> FormatBackend {
self.format_backend.unwrap_or_default()
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.12.11"
version = "0.12.12"
publish = false
authors = { workspace = true }
edition = { workspace = true }

3
crates/ty/docs/cli.md generated
View File

@@ -60,8 +60,9 @@ over all configuration files.</p>
</dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p>
<p>Possible values:</p>
<ul>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints [default]</li>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints (default)</li>
<li><code>concise</code>: Print diagnostics concisely, one per line</li>
<li><code>gitlab</code>: Print diagnostics in the JSON format expected by GitLab Code Quality reports</li>
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>

View File

@@ -144,7 +144,7 @@ If left unspecified, ty will try to detect common project layouts and initialize
* if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path
* otherwise, default to `.` (flat layout)
Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file),
Besides, if a `./python` or `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` or `__init__.pyi` file),
it will also be included in the first party search path.
**Default value**: `null`

View File

@@ -306,7 +306,7 @@ impl clap::Args for RulesArg {
/// The diagnostic output format.
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum OutputFormat {
/// Print diagnostics verbosely, with context and helpful hints \[default\].
/// Print diagnostics verbosely, with context and helpful hints (default).
///
/// Diagnostic messages may include additional context and
/// annotations on the input to help understand the message.
@@ -321,6 +321,9 @@ pub enum OutputFormat {
/// dropped.
#[value(name = "concise")]
Concise,
/// Print diagnostics in the JSON format expected by GitLab Code Quality reports.
#[value(name = "gitlab")]
Gitlab,
}
impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
@@ -328,6 +331,7 @@ impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
match format {
OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise,
OutputFormat::Gitlab => Self::Gitlab,
}
}
}

View File

@@ -21,7 +21,7 @@ use clap::{CommandFactory, Parser};
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, DisplayDiagnostics, Severity};
use ruff_db::files::File;
use ruff_db::max_parallelism;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
@@ -319,37 +319,48 @@ impl MainLoop {
return Ok(ExitStatus::Success);
}
let is_human_readable = terminal_settings.output_format.is_human_readable();
if result.is_empty() {
writeln!(
self.printer.stream_for_success_summary(),
"{}",
"All checks passed!".green().bold()
)?;
if is_human_readable {
writeln!(
self.printer.stream_for_success_summary(),
"{}",
"All checks passed!".green().bold()
)?;
}
if self.watcher.is_none() {
return Ok(ExitStatus::Success);
}
} else {
let mut max_severity = Severity::Info;
let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock();
for diagnostic in result {
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
if stdout.is_enabled() {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
}
let max_severity = result
.iter()
.map(Diagnostic::severity)
.max()
.unwrap_or(Severity::Info);
max_severity = max_severity.max(diagnostic.severity());
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
if stdout.is_enabled() {
write!(
stdout,
"{}",
DisplayDiagnostics::new(db, &display_config, &result)
)?;
}
writeln!(
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;
if is_human_readable {
writeln!(
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;
}
if max_severity.is_fatal() {
tracing::warn!(

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