Compare commits

..

59 Commits

Author SHA1 Message Date
Brent Westbrook
c2bc15bc15 Bump 0.12.11 (#20136) 2025-08-28 09:45:01 -04:00
David Peter
e586f6dcc4 [ty] Benchmarks for problematic implicit instance attributes cases (#20133)
## Summary

Add regression benchmarks for the problematic cases in
https://github.com/astral-sh/ty/issues/758. I'd like to merge this
before https://github.com/astral-sh/ruff/pull/20128 to measure the
impact (local tests show that this will "solve" both cases).
2025-08-28 15:25:25 +02:00
Takayuki Maeda
76a6b7e3e2 [pyflakes] Fix allowed-unused-imports matching for top-level modules (F401) (#20115)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

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

Fixes #19664

Fix allowed unused imports matching for top-level modules.

I've simply replaced `from_dotted_name` with `user_defined`. Since
QualifiedName for imports is created in
crates/ruff_python_semantic/src/imports.rs, I guess it's acceptable to
use `user_defined` here. Please tell me if there is better way.


0c5089ed9e/crates/ruff_python_semantic/src/imports.rs (L62)

## Test Plan

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

I've added a snapshot test
`f401_allowed_unused_imports_top_level_module`.
2025-08-28 13:02:50 +00:00
Brent Westbrook
1ce65714c0 Move GitLab output rendering to ruff_db (#20117)
## Summary

This PR is a first step toward adding a GitLab output format to ty. It
converts the `GitlabEmitter` from `ruff_linter` to a `GitlabRenderer` in
`ruff_db` and updates its implementation to handle non-Ruff files and
diagnostics without primary spans. I tried to break up the changes here
so that they're easy to review commit-by-commit, or at least in groups
of commits:
- [preparatory changes in-place in `ruff_linter` and a `ruff_db`
skeleton](0761b73a61)
- [moving the code over with no implementation changes mixed
in](0761b73a61..8f909ea0bb)
- [tidying up the code now in
`ruff_db`](9f047c4f9f..e5e217fcd6)

This wasn't strictly necessary, but I also added some `Serialize`
structs instead of calling `json!` to make it a little clearer that we
weren't modifying the schema (e4c4bee35d).

I plan to follow this up with a separate PR exposing this output format
in the ty CLI, which should be quite straightforward.

## Test Plan

Existing tests, especially the two that show up in the diff as renamed
nearly without changes
2025-08-28 08:56:33 -04:00
Shaygan Hooshyari
d9aaacd01f [ty] Evaluate reachability of non-definitely-bound to Ambiguous (#19579)
## Summary

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

If the expression (or any child expressions) is not definitely bound the
reachability constraint evaluation is determined as ambiguous.

This fixes the infinite cycles panic in the following code:

```py
from typing import Literal

class Toggle:
    def __init__(self: "Toggle"):
        if not self.x:
            self.x: Literal[True] = True
```

Credit of this solution is for David.

## Test Plan

- Added a test case with too many cycle iterations panic.
- Previous tests.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-08-28 14:34:49 +02:00
Jelle Zijlstra
18eaa659c1 [ty] Introduce a representation for the top/bottom materialization of an invariant generic (#20076)
Part of #994. This adds a new field to the Specialization struct to
record when we're dealing with the top or bottom materialization of an
invariant generic. It also implements subtyping and assignability for
these objects.

Next planned steps after this is done are to implement other operations
on top/bottom materializations; probably attribute access is an
important one.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-08-27 17:53:57 -07:00
Amethyst Reese
af259faed5 [flake8-async] Implement blocking-http-call-httpx (ASYNC212) (#20091)
## Summary

Adds new rule to find and report use of `httpx.Client` in synchronous
functions.

See issue #8451

## Test Plan

New snapshots for `ASYNC212.py` with `cargo insta test`.
2025-08-27 15:19:01 -07:00
Leandro Braga
d75ef3823c [ty] print diagnostics with fully qualified name to disambiguate some cases (#19850)
There are some situations that we have a confusing diagnostics due to
identical class names.

## Class with same name from different modules

```python
import pandas
import polars

df: pandas.DataFrame = polars.DataFrame()
```

This yields the following error:

**Actual:**
error: [invalid-assignment] "Object of type `DataFrame` is not
assignable to `DataFrame`"
**Expected**:
error: [invalid-assignment] "Object of type `polars.DataFrame` is not
assignable to `pandas.DataFrame`"

## Nested classes

```python
from enum import Enum

class A:
    class B(Enum):
        ACTIVE = "active"
        INACTIVE = "inactive"

class C:
    class B(Enum):
        ACTIVE = "active"
        INACTIVE = "inactive"
```

**Actual**:
error: [invalid-assignment] "Object of type `Literal[B.ACTIVE]` is not
assignable to `B`"
**Expected**:
error: [invalid-assignment] "Object of type
`Literal[my_module.C.B.ACTIVE]` is not assignable to `my_module.A.B`"

## Solution

In this MR we added an heuristics to detect when to use a fully
qualified name:
- There is an invalid assignment and;
- They are two different classes and;
- They have the same name

The fully qualified name always includes:
- module name
- nested classes name
- actual class name

There was no `QualifiedDisplay` so I had to implement it from scratch.
I'm very new to the codebase, so I might have done things inefficiently,
so I appreciate feedback.

Should we pre-compute the fully qualified name or do it on demand? 

## Not implemented

### Function-local classes

Should we approach this in a different PR?

**Example**:
```python 
# t.py
from __future__ import annotations


def function() -> A:
    class A:
        pass

    return A()


class A:
    pass


a: A = function()
```

#### mypy

```console
t.py:8: error: Incompatible return value type (got "t.A@5", expected "t.A")  [return-value]
```

From my testing the 5 in `A@5` comes from the like number. 

#### ty

```console
error[invalid-return-type]: Return type does not match returned value
 --> t.py:4:19
  |
4 | def function() -> A:
  |                   - Expected `A` because of return type
5 |     class A:
6 |         pass
7 |
8 |     return A()
  |            ^^^ expected `A`, found `A`
  |
info: rule `invalid-return-type` is enabled by default
```

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

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-08-27 20:46:07 +00:00
Dan Parizher
89ca493fd9 [ruff] Preserve relative whitespace in multi-line expressions (RUF033) (#19647)
## Summary

Fixes #19581

I decided to add in a `indent_first_line` function into
[`textwrap.rs`](https://github.com/astral-sh/ruff/blob/main/crates/ruff_python_trivia/src/textwrap.rs),
as it solely focuses on text manipulation utilities. It follows the same
design as `indent()`, and there may be situations in the future where it
can be reused as well.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-08-27 19:15:44 +00:00
David Peter
4b80f5fa4f [ty] Optimize TDD atom ordering (#20098)
## Summary

While looking at some logging output that I added to
`ReachabilityConstraintBuilder::add_and_constraint` in order to debug
https://github.com/astral-sh/ty/issues/1091, I noticed that it seemed to
suggest that the TDD was built in an imbalanced way for code like the
following, where we have a sequence of non-nested `if` conditions:

```py
def f(t1, t2, t3, t4, …):
    x = 0
    if t1:
        x = 1
    if t2:
        x = 2
    if t3:
        x = 3
    if t4:
        x = 4
    …
```

To understand this a bit better, I added some code to the
`ReachabilityConstraintBuilder` to render the resulting TDD. On `main`,
we get a tree that looks like the following, where you can see a pattern
of N sub-trees that grow linearly with N (number of `if` statements).
This results in an overall tree structure that has N² nodes (see graph
below):

<img alt="normal order"
src="https://github.com/user-attachments/assets/aab40ce9-e82a-4fcd-823a-811f05f15f66"
/>

If we zoom in to one of these subgraphs, we can see what the problem is.
When we add new constraints that represent combinations like `t1 AND ~t2
AND ~t3 AND t4 AND …`, they start with the evaluation of "early"
conditions (`t1`, `t2`, …). This means that we have to create new
subgraphs for each new `if` condition because there is little sharing
with the previous structure. We evaluate the Boolean condition in a
right-associative way: `t1 AND (~t2 AND (~t3 AND t4)))`:

<img width="500" align="center"
src="https://github.com/user-attachments/assets/31ea7182-9e00-4975-83df-d980464f545d"
/>

If we change the ordering of TDD atoms, we can change that to a
left-associative evaluation: `(((t1 AND ~t2) AND ~t3) AND t4) …`. This
means that we can re-use previous subgraphs `(t1 AND ~t2)`, which
results in a much more compact graph structure overall (note how "late"
conditions are now at the top, and "early" conditions are further down
in the graph):

<img alt="reverse order"
src="https://github.com/user-attachments/assets/96a6b7c1-3d35-4192-a917-0b2d24c6b144"
/>

If we count the number of TDD nodes for a growing number if `if`
statements, we can see that this change results in a slower growth. It's
worth noting that the growth is still superlinear, though:

<img width="800" height="600" alt="plot"
src="https://github.com/user-attachments/assets/22e8394f-e74e-4a9e-9687-0d41f94f2303"
/>

On the actual code from the referenced ticket (the `t_main.py` file
reduced to its main function, with the main function limited to 2000
lines instead of 11000 to allow the version on `main` to run to
completion), the effect is much more dramatic. Instead of 26 million TDD
nodes (`main`), we now only create 250 thousand (this branch), which is
slightly less than 1%.

The change in this PR allows us to build the semantic index and
type-check the problematic `t_main.py` file in
https://github.com/astral-sh/ty/issues/1091 in 9 seconds. This is still
not great, but an obvious improvement compared to running out of memory
after *minutes* of execution.

An open question remains whether this change is beneficial for all kinds
of code patterns, or just this linear sequence of `if` statements. It
does not seem unreasonable to think that referring to "earlier"
conditions is generally a good idea, but I learned from Doug that it's
generally not possible to find a TDD-construction heuristic that is
non-pathological for all kinds of inputs. Fortunately, it seems like
this change here results in performance improvements across *all of our
benchmarks*, which should increase the confidence in this change:

| Benchmark           | Improvement |
|---------------------|-------------------------|
| hydra-zen           | +13%                    |
| DateType            | +5%                     |
| sympy (walltime)    | +4%                     |
| attrs               | +4%                     |
| pydantic (walltime) | +2%                     |
| pandas (walltime)   | +2%                     |
| altair (walltime)   | +2%                     |
| static-frame        | +2%                     |
| anyio               | +1%                     |
| freqtrade           | +1%                     |
| colour-science      | +1%                     |
| tanjun              | +1%                     |

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

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
2025-08-27 20:42:09 +02:00
Wei Lee
5663426d73 [airflow] Extend AIR311 and AIR312 rules (#20082)
<!--
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? -->

Extend the following rules.

### AIR311
* `airflow.sensors.base.BaseSensorOperator` →
airflow.sdk.bases.sensor.BaseSensorOperator`
* `airflow.sensors.base.PokeReturnValue` →
airflow.sdk.bases.sensor.PokeReturnValue`
* `airflow.sensors.base.poke_mode_only` →
airflow.sdk.bases.sensor.poke_mode_only`
* `airflow.decorators.base.DecoratedOperator` →
airflow.sdk.bases.decorator.DecoratedOperator`
* `airflow.models.param.Param` → airflow.sdk.definitions.param.Param`
* `airflow.decorators.base.DecoratedMappedOperator` →
`airflow.sdk.bases.decorator.DecoratedMappedOperator`
* `airflow.decorators.base.DecoratedOperator` →
`airflow.sdk.bases.decorator.DecoratedOperator`
* `airflow.decorators.base.TaskDecorator` →
`airflow.sdk.bases.decorator.TaskDecorator`
* `airflow.decorators.base.get_unique_task_id` →
`airflow.sdk.bases.decorator.get_unique_task_id`
* `airflow.decorators.base.task_decorator_factory` →
`airflow.sdk.bases.decorator.task_decorator_factory`


### AIR312
* `airflow.sensors.bash.BashSensor` →
`airflow.providers.standard.sensor.bash.BashSensor`
* `airflow.sensors.python.PythonSensor` →
`airflow.providers.standard.sensors.python.PythonSensor`



## Test Plan

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

update the test fixture accordingly in the second commit and reorg in
the third
2025-08-27 14:11:22 -04:00
David Peter
0b3548755c [ty] Preserve qualifiers when accessing attributes on unions/intersections (#20114)
## Summary

Properly preserve type qualifiers when accessing attributes on unions
and intersections. This is a prerequisite for
https://github.com/astral-sh/ruff/pull/19579.

Also fix a completely wrong implementation of
`map_with_boundness_and_qualifiers`. It now closely follows
`map_with_boundness` (just above).

## Test Plan

I thought about it, but didn't find any easy way to test this. This only
affected `Type::member`. Things like validation of attribute writes
(where type qualifiers like `ClassVar` and `Final` are important) were
already handling things correctly.
2025-08-27 20:01:45 +02:00
Alex Waygood
ce1dc21e7e [ty] Fix the inferred interface of specialized generic protocols (#19866) 2025-08-27 18:16:15 +01:00
Alex Waygood
7d0c8e045c [ty] Infer slightly more precise types for comprehensions (#20111) 2025-08-27 13:21:47 +01:00
Alex Waygood
d71518b369 [ty] Add more tests for protocols (#20095)
Co-authored-by: Shunsuke Shibayama <sbym1346@gmail.com>
2025-08-27 12:56:14 +01:00
Carl Meyer
9ab276b345 [ty] don't eagerly unpack aliases in user-authored unions (#20055)
## Summary

Add a subtly different test case for recursive PEP 695 type aliases,
which does require that we relax our union simplification, so we don't
eagerly unpack aliases from user-provided union annotations.

## Test Plan

Added mdtest.
2025-08-26 16:29:45 -07:00
chiri
a60fb3f2c8 [flake8-use-pathlib] Update links to the table showing the correspondence between os and pathlib (#20103)
<!--
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

Part of https://github.com/astral-sh/ruff/pull/20100 |
https://github.com/astral-sh/ruff/pull/20100#issuecomment-3225349156
2025-08-26 17:33:33 -04:00
chiri
f558bf721c [flake8-use-pathlib] Make PTH100 fix unsafe because it can change behavior (#20100)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary
Fixes https://github.com/astral-sh/ruff/issues/20088
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

`cargo nextest run flake8_use_pathlib`

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-08-26 18:59:12 +00:00
chiri
ea1c080881 [flake8-use-pathlib] Delete unused Rule::OsSymlink enabled check (#20099)
<!--
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
Part of #20009 (i forgot to delete it in this PR)
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
2025-08-26 12:05:52 -04:00
Renkai Ge
73720c73be [ty] Add search paths info to unresolved import diagnostics (#20040)
Fixes https://github.com/astral-sh/ty/issues/457

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-08-26 11:01:16 -04:00
Hamir Mahal
136abace92 [flake8-logging-format] Add auto-fix for f-string logging calls (G004) (#19303)
Closes #19302

<!--
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 adds an auto-fix for `Logging statement uses f-string` Ruff G004,
so users don't have to resolve it manually.
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
I ran the auto-fixes on a Python file locally and and it worked as
expected.
<!-- How was it tested? -->

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-08-26 10:51:24 -04:00
Brent Westbrook
bc7274d148 Add a ScopeKind for the __class__ cell (#20048)
Summary
--

This PR aims to resolve (or help to resolve) #18442 and #19357 by
encoding the CPython semantics around the `__class__` cell in our
semantic model. Namely,

> `__class__` is an implicit closure reference created by the compiler
if any methods in a class body refer to either `__class__` or super.

from the Python
[docs](https://docs.python.org/3/reference/datamodel.html#creating-the-class-object).

As noted in the variant docs by @AlexWaygood, we don't fully model this
behavior, opting always to create the `__class__` cell binding in a new
`ScopeKind::DunderClassCell` around each method definition, without
checking if any method in the class body actually refers to `__class__`
or `super`.

As such, this PR fixes #18442 but not #19357.

Test Plan
--

Existing tests, plus the tests from #19783, which now pass without any
rule-specific code.

Note that we opted not to alter the behavior of F841 here because
flagging `__class__` in these cases still seems helpful. See the
discussion in
https://github.com/astral-sh/ruff/pull/20048#discussion_r2296252395 and
in the test comments for more information.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Mikko Leppänen <mleppan23@gmail.com>
2025-08-26 09:49:08 -04:00
Avasam
911d5cc973 Fix incorrect D413 links in docstrings convention FAQ (#20089)
## Summary

D413 in this section was incorrectly linking to D410.

I haven't checked if this issue happens anywhere else in the docs.

## Test Plan

Look at docs
2025-08-26 10:24:58 +05:30
Matthew Mckee
8d6dc7d3a3 [ty] Refactor inlay hints structure to use separate parts (#20052)
## Summary

Our internal inlay hints structure (`ty_ide::InlayHint`) now more
closely resembles `lsp_types::InlayHint`.

This mainly allows us to convert to `lsp_types::InlayHint` with less
hassle, but it also allows us to manage the different parts of the inlay
hint better, which in the future will allow us to implement features
like goto on the type part of the type inlay hint.

It also really isn't important to store a specific `Type` instance in
the `InlayHintContent`. So we remove this and use `InlayHintLabel`
instead which just shows the representation of the type (along with
other information).

We see a similar structure used in rust-analyzer too.
2025-08-26 10:21:31 +05:30
Dylan
ef4897f9f3 [ty] Add support for PEP 750 t-strings (#20085)
This PR attempts to adds support for inferring`string.templatelib.Template` for t-string literals.
2025-08-25 18:49:49 +00:00
Alex Waygood
ecf3c4ca11 [ty] Add support for PEP 800 (#20084) 2025-08-25 19:39:05 +01:00
Carl Meyer
33c5f6f4f8 [ty] don't mark entire type-alias scopes as Deferred (#20086)
## Summary

This has been here for awhile (since our initial PEP 695 type alias
support) but isn't really correct. The right-hand-side of a PEP 695 type
alias is a distinct scope, and we don't mark it as an "eager" nested
scope, so it automatically gets "deferred" resolution of names from
outer scopes (just like a nested function). Thus it's
redundant/unnecessary for us to use `DeferredExpressionState::Deferred`
for resolving that RHS expression -- that's for deferring resolution of
individual names within a scope. Using it here causes us to wrongly
ignore applicable outer-scope narrowing.

## Test Plan

Added mdtest that failed before this PR (the second snippet -- the first
snippet always passed.)
2025-08-25 11:32:18 -07:00
github-actions[bot]
ba47010150 [ty] Sync vendored typeshed stubs (#20083)
Co-authored-by: typeshedbot <>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-08-25 17:01:51 +00:00
Wei Lee
db423ee978 [airflow] replace wrong path airflow.io.stroage as airflow.io.store (AIR311) (#20081)
## Summary

`airflow.io.storage` is not the correct path. it should be
`airflow.io.store` instead
2025-08-25 10:15:34 -05:00
Alex Waygood
a04823cfad [ty] Completely ignore typeshed's stub for Any (#20079) 2025-08-25 15:27:55 +01:00
Brent Westbrook
d0bcf56bd9 Improve diff rendering for notebooks (#20036)
## Summary

As noted in a code TODO, our `Diff` rendering code previously didn't
have any
special handling for notebooks. This was particularly obvious when the
diffs
were rendered right next to the corresponding diagnostic because the
diagnostic
used cell-based line numbers, while the diff was still using line
numbers from
the concatenated source. This PR updates the diff rendering to handle
notebooks
too.

The main improvements shown in the example below are:

- Line numbers are now remapped to be relative to their cell
- Context lines from other cells are suppressed

```
error[unused-import][*]: `math` imported but unused                             
 --> notebook.ipynb:cell 2:2:8                                                  
  |                                                                             
1 | # cell 2                                                                    
2 | import math                                                                 
  |        ^^^^                                                                 
3 |                                                                             
4 | print('hello world')                                                        
  |                                                                             
help: Remove unused import: `math`                                              
                                                                                
ℹ Safe fix                                                                      
1 1 | # cell 2                                                                  
2   |-import math                                                               
3 2 |                                                                           
4 3 | print('hello world')                                                      
```

I tried a few different approaches here before finally just splitting
the notebook into separate text ranges by cell and diffing each one
separately. It seems to work and passes all of our tests, but I don't
know if it's actually enforced anywhere that a single edit doesn't span
cells. Such an edit would silently be dropped right now since it would
fail the `contains_range` check. I also feel like I may have overlooked
an existing way to partition a file into cells like this.

## Test Plan

Existing notebook tests, plus a new one in `ruff_db`
2025-08-25 09:20:42 -04:00
Eric Jolibois
f9bbee33f6 [ty] validate constructor call of TypedDict (#19810)
## Summary
Implement validation for `TypedDict` constructor calls and dictionary
literal assignments, including support for `total=False` and proper
field management.
Also add support for `Required` and `NotRequired` type qualifiers in
`TypedDict` classes, along with proper inheritance behavior and the
`total=` parameter.
Support both constructor calls and dict literal syntax

part of https://github.com/astral-sh/ty/issues/154

### Basic Required Field Validation
```py
class Person(TypedDict):
    name: str
    age: int | None

# Error: Missing required field 'name' in TypedDict `Person` constructor
incomplete = Person(age=25)

# Error: Invalid argument to key "name" with declared type `str` on TypedDict `Person`
wrong_type = Person(name=123, age=25)

# Error: Invalid key access on TypedDict `Person`: Unknown key "extra"
extra_field = Person(name="Bob", age=25, extra=True)
```
<img width="773" height="191" alt="Screenshot 2025-08-07 at 17 59 22"
src="https://github.com/user-attachments/assets/79076d98-e85f-4495-93d6-a731aa72a5c9"
/>

### Support for `total=False`
```py
class OptionalPerson(TypedDict, total=False):
    name: str
    age: int | None

# All valid - all fields are optional with total=False
charlie = OptionalPerson()
david = OptionalPerson(name="David")
emily = OptionalPerson(age=30)
frank = OptionalPerson(name="Frank", age=25)

# But type validation and extra fields still apply
invalid_type = OptionalPerson(name=123)  # Error: Invalid argument type
invalid_extra = OptionalPerson(extra=True)  # Error: Invalid key access
```

### Dictionary Literal Validation
```py
# Type checking works for both constructors and dict literals
person: Person = {"name": "Alice", "age": 30}

reveal_type(person["name"])  # revealed: str
reveal_type(person["age"])   # revealed: int | None

# Error: Invalid key access on TypedDict `Person`: Unknown key "non_existing"
reveal_type(person["non_existing"])  # revealed: Unknown
```

### `Required`, `NotRequired`, `total`
```python
from typing import TypedDict
from typing_extensions import Required, NotRequired

class PartialUser(TypedDict, total=False):
    name: Required[str]      # Required despite total=False
    age: int                 # Optional due to total=False
    email: NotRequired[str]  # Explicitly optional (redundant)

class User(TypedDict):
    name: Required[str]      # Explicitly required (redundant)
    age: int                 # Required due to total=True
    bio: NotRequired[str]    # Optional despite total=True

# Valid constructions
partial = PartialUser(name="Alice")  # name required, age optional
full = User(name="Bob", age=25)      # name and age required, bio optional

# Inheritance maintains original field requirements
class Employee(PartialUser):
    department: str                  # Required (new field)
    # name: still Required (inherited)
    # age: still optional (inherited)

emp = Employee(name="Charlie", department="Engineering")  # 
Employee(department="Engineering")  # 
e: Employee = {"age": 1}  # 
```

<img width="898" height="683" alt="Screenshot 2025-08-11 at 22 02 57"
src="https://github.com/user-attachments/assets/4c1b18cd-cb2e-493a-a948-51589d121738"
/>

## Implementation
The implementation reuses existing validation logic done in
https://github.com/astral-sh/ruff/pull/19782

### ℹ️ Why I did NOT synthesize an `__init__` for `TypedDict`:

`TypedDict` inherits `dict.__init__(self, *args, **kwargs)` that accepts
all arguments.
The type resolution system finds this inherited signature **before**
looking for synthesized members.
So `own_synthesized_member()` is never called because a signature
already exists.

To force synthesis, you'd have to override Python’s inheritance
mechanism, which would break compatibility with the existing ecosystem.

This is why I went with ad-hoc validation. IMO it's the only viable
approach that respects Python’s
inheritance semantics while providing the required validation.

### Refacto of `Field`

**Before:**
```rust
struct Field<'db> {
    declared_ty: Type<'db>,
    default_ty: Option<Type<'db>>,     // NamedTuple and dataclass only
    init_only: bool,                   // dataclass only  
    init: bool,                        // dataclass only
    is_required: Option<bool>,         // TypedDict only
}
```

**After:**
```rust
struct Field<'db> {
    declared_ty: Type<'db>,
    kind: FieldKind<'db>,
}

enum FieldKind<'db> {
    NamedTuple { default_ty: Option<Type<'db>> },
    Dataclass { default_ty: Option<Type<'db>>, init_only: bool, init: bool },
    TypedDict { is_required: bool },
}
```

## Test Plan
Updated Markdown tests

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-08-25 14:45:52 +02:00
Dhruv Manilawala
376e3ff395 [ty] Limit argument expansion size for overload call evaluation (#20041)
## Summary

This PR limits the argument type expansion size for an overload call
evaluation to 512.

The limit chosen is arbitrary but I've taken the 256 limit from Pyright
into account and bumped it x2 to start with.

Initially, I actually started out by trying to refactor the entire
argument type expansion to be lazy. Currently, expanding a single
argument at any position eagerly creates the combination (argument
lists) and returns that (`Vec<CallArguments>`) but I thought we could
make it lazier by converting the return type of `expand` from
`Iterator<Item = Vec<CallArguments>>` to `Iterator<Item = Iterator<Item
= CallArguments>>` but that's proving to be difficult to implement
mainly because we **need** to maintain the previous expansion to
generate the next expansion which is the main reason to use
`std::iter::successors` in the first place.

Another approach would be to eagerly expand all the argument types and
then use the `combinations` from `itertools` to generate the
combinations but we would need to find the "boundary" between arguments
lists produced from expanding argument at position 1 and position 2
because that's important for the algorithm.

Closes: https://github.com/astral-sh/ty/issues/868

## Test Plan

Add test case to demonstrate the limit along with the diagnostic
snapshot stating that the limit has been reached.
2025-08-25 09:43:04 +00:00
renovate[bot]
b57cc5be33 Update actions/checkout digest to ff7abcd (#20061)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| actions/checkout | action | digest | `08c6903` -> `ff7abcd` |

---

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

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-08-25 11:04:07 +05:30
renovate[bot]
3c1fe12259 Update Rust crate regex-automata to v0.4.10 (#20068)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[regex-automata](https://redirect.github.com/rust-lang/regex/tree/master/regex-automata)
([source](https://redirect.github.com/rust-lang/regex)) |
workspace.dependencies | patch | `0.4.9` -> `0.4.10` |

---

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

---

### Release Notes

<details>
<summary>rust-lang/regex (regex-automata)</summary>

###
[`v0.4.10`](https://redirect.github.com/rust-lang/regex/compare/regex-automata-0.4.9...regex-automata-0.4.10)

[Compare
Source](https://redirect.github.com/rust-lang/regex/compare/regex-automata-0.4.9...regex-automata-0.4.10)

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:44:59 +05:30
renovate[bot]
e4289deb5a Update Rust crate regex to v1.11.2 (#20067)
This PR contains the following updates:

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

---

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

---

### Release Notes

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

###
[`v1.11.2`](https://redirect.github.com/rust-lang/regex/blob/HEAD/CHANGELOG.md#1112-2025-08-24)

[Compare
Source](https://redirect.github.com/rust-lang/regex/compare/1.11.1...1.11.2)

\===================
This is a new patch release of `regex` with some minor fixes. A larger
number
of typo or lint fix patches were merged. Also, we now finally recommend
using
`std::sync::LazyLock`.

Improvements:

- [BUG
#&#8203;1217](https://redirect.github.com/rust-lang/regex/issues/1217):
  Switch recommendation from `once_cell` to `std::sync::LazyLock`.
- [BUG
#&#8203;1225](https://redirect.github.com/rust-lang/regex/issues/1225):
  Add `DFA::set_prefilter` to `regex-automata`.

Bug fixes:

- [BUG
#&#8203;1165](https://redirect.github.com/rust-lang/regex/pull/1150):
Remove `std` dependency from `perf-literal-multisubstring` crate
feature.
- [BUG
#&#8203;1165](https://redirect.github.com/rust-lang/regex/pull/1165):
  Clarify the meaning of `(?R)$` in the documentation.
- [BUG
#&#8203;1281](https://redirect.github.com/rust-lang/regex/pull/1281):
Remove `fuzz/` and `record/` directories from published crate on
crates.io.

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:44:47 +05:30
renovate[bot]
dbbcb7f452 Update cargo-bins/cargo-binstall action to v1.15.1 (#20075)
This PR contains the following updates:

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

---

> [!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.1`](https://redirect.github.com/cargo-bins/cargo-binstall/releases/tag/v1.15.1)

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

*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:

- fix failure to create settings manifest file
([#&#8203;2268](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2268)
[#&#8203;2271](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2271))
- fix race condition in creating, loading and writing of settings
manifest file
([#&#8203;2272](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2272))
- fix infinite hang on `--self-install` due to the quickinstall consent
([#&#8203;2269](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2269)
[#&#8203;2273](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2273))

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

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

*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:

- confirm on the first time using quickinstall
([#&#8203;2223](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2223))

##### Other changes:

- upgrade dependencies

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:43:27 +05:30
renovate[bot]
c6dfdb1d39 Update Rust crate url to v2.5.7 (#20073)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [url](https://redirect.github.com/servo/rust-url) |
workspace.dependencies | patch | `2.5.4` -> `2.5.7` |

---

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

---

### Release Notes

<details>
<summary>servo/rust-url (url)</summary>

###
[`v2.5.5`](https://redirect.github.com/servo/rust-url/releases/tag/v2.5.5)

[Compare
Source](https://redirect.github.com/servo/rust-url/compare/v2.5.4...v2.5.5)

##### What's Changed

- ci: downgrade crates when building for Rust 1.67.0 by
[@&#8203;mxinden](https://redirect.github.com/mxinden) in
[https://github.com/servo/rust-url/pull/1003](https://redirect.github.com/servo/rust-url/pull/1003)
- ci: run unit tests with sanitizers by
[@&#8203;mxinden](https://redirect.github.com/mxinden) in
[https://github.com/servo/rust-url/pull/1002](https://redirect.github.com/servo/rust-url/pull/1002)
- fix small typo by [@&#8203;hkBst](https://redirect.github.com/hkBst)
in
[https://github.com/servo/rust-url/pull/1011](https://redirect.github.com/servo/rust-url/pull/1011)
- chore: fix clippy errors on main by
[@&#8203;dsherret](https://redirect.github.com/dsherret) in
[https://github.com/servo/rust-url/pull/1019](https://redirect.github.com/servo/rust-url/pull/1019)
- perf: remove heap allocation in parse\_query by
[@&#8203;dsherret](https://redirect.github.com/dsherret) in
[https://github.com/servo/rust-url/pull/1020](https://redirect.github.com/servo/rust-url/pull/1020)
- perf: slightly improve parsing a port by
[@&#8203;dsherret](https://redirect.github.com/dsherret) in
[https://github.com/servo/rust-url/pull/1022](https://redirect.github.com/servo/rust-url/pull/1022)
- perf: improve to\_file\_path() by
[@&#8203;dsherret](https://redirect.github.com/dsherret) in
[https://github.com/servo/rust-url/pull/1018](https://redirect.github.com/servo/rust-url/pull/1018)
- perf: make parse\_scheme slightly faster by
[@&#8203;dsherret](https://redirect.github.com/dsherret) in
[https://github.com/servo/rust-url/pull/1025](https://redirect.github.com/servo/rust-url/pull/1025)
- update LICENSE-MIT by
[@&#8203;wmjae](https://redirect.github.com/wmjae) in
[https://github.com/servo/rust-url/pull/1029](https://redirect.github.com/servo/rust-url/pull/1029)
- perf: url encode path segments in longer string slices by
[@&#8203;dsherret](https://redirect.github.com/dsherret) in
[https://github.com/servo/rust-url/pull/1026](https://redirect.github.com/servo/rust-url/pull/1026)
- Disable the default features on serde by
[@&#8203;rilipco](https://redirect.github.com/rilipco) in
[https://github.com/servo/rust-url/pull/1033](https://redirect.github.com/servo/rust-url/pull/1033)
- docs: base url relative join by
[@&#8203;tisonkun](https://redirect.github.com/tisonkun) in
[https://github.com/servo/rust-url/pull/1013](https://redirect.github.com/servo/rust-url/pull/1013)
- perf: remove heap allocation in parse\_host by
[@&#8203;dsherret](https://redirect.github.com/dsherret) in
[https://github.com/servo/rust-url/pull/1021](https://redirect.github.com/servo/rust-url/pull/1021)
- Update tests to Unicode 16.0 by
[@&#8203;hsivonen](https://redirect.github.com/hsivonen) in
[https://github.com/servo/rust-url/pull/1045](https://redirect.github.com/servo/rust-url/pull/1045)
- Add some some basic functions to `Mime` by
[@&#8203;mrobinson](https://redirect.github.com/mrobinson) in
[https://github.com/servo/rust-url/pull/1047](https://redirect.github.com/servo/rust-url/pull/1047)
- ran `cargo clippy --fix -- -Wclippy::use_self` by
[@&#8203;mrobinson](https://redirect.github.com/mrobinson) in
[https://github.com/servo/rust-url/pull/1048](https://redirect.github.com/servo/rust-url/pull/1048)
- Fix MSRV and clippy CI by
[@&#8203;Manishearth](https://redirect.github.com/Manishearth) in
[https://github.com/servo/rust-url/pull/1058](https://redirect.github.com/servo/rust-url/pull/1058)
- Update `Url::domain` docs to show that it includes subdomain by
[@&#8203;supercoolspy](https://redirect.github.com/supercoolspy) in
[https://github.com/servo/rust-url/pull/1057](https://redirect.github.com/servo/rust-url/pull/1057)
- set\_hostname should error when encountering colon ':' by
[@&#8203;edgul](https://redirect.github.com/edgul) in
[https://github.com/servo/rust-url/pull/1060](https://redirect.github.com/servo/rust-url/pull/1060)
- version bump to 2.5.5 by
[@&#8203;edgul](https://redirect.github.com/edgul) in
[https://github.com/servo/rust-url/pull/1061](https://redirect.github.com/servo/rust-url/pull/1061)

##### New Contributors

- [@&#8203;mxinden](https://redirect.github.com/mxinden) made their
first contribution in
[https://github.com/servo/rust-url/pull/1003](https://redirect.github.com/servo/rust-url/pull/1003)
- [@&#8203;hkBst](https://redirect.github.com/hkBst) made their first
contribution in
[https://github.com/servo/rust-url/pull/1011](https://redirect.github.com/servo/rust-url/pull/1011)
- [@&#8203;wmjae](https://redirect.github.com/wmjae) made their first
contribution in
[https://github.com/servo/rust-url/pull/1029](https://redirect.github.com/servo/rust-url/pull/1029)
- [@&#8203;rilipco](https://redirect.github.com/rilipco) made their
first contribution in
[https://github.com/servo/rust-url/pull/1033](https://redirect.github.com/servo/rust-url/pull/1033)
- [@&#8203;tisonkun](https://redirect.github.com/tisonkun) made their
first contribution in
[https://github.com/servo/rust-url/pull/1013](https://redirect.github.com/servo/rust-url/pull/1013)
- [@&#8203;supercoolspy](https://redirect.github.com/supercoolspy) made
their first contribution in
[https://github.com/servo/rust-url/pull/1057](https://redirect.github.com/servo/rust-url/pull/1057)

**Full Changelog**:
https://github.com/servo/rust-url/compare/v2.5.4...v2.5.5

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:42:38 +05:30
renovate[bot]
c65029f9a5 Update Rust crate syn to v2.0.106 (#20070)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [syn](https://redirect.github.com/dtolnay/syn) |
workspace.dependencies | patch | `2.0.104` -> `2.0.106` |

---

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

---

### Release Notes

<details>
<summary>dtolnay/syn (syn)</summary>

###
[`v2.0.106`](https://redirect.github.com/dtolnay/syn/releases/tag/2.0.106)

[Compare
Source](https://redirect.github.com/dtolnay/syn/compare/2.0.105...2.0.106)

- Replace `~const` syntax with `[const]` conditionally const syntax in
trait bounds
([#&#8203;1896](https://redirect.github.com/dtolnay/syn/issues/1896),
[rust-lang/rust#139858](https://redirect.github.com/rust-lang/rust/pull/139858))
- Support conditionally const impl Trait types
([#&#8203;1897](https://redirect.github.com/dtolnay/syn/issues/1897))
- Reject polarity modifier and lifetime binder used in the same trait
bound
([#&#8203;1899](https://redirect.github.com/dtolnay/syn/issues/1899),
[rust-lang/rust#127054](https://redirect.github.com/rust-lang/rust/pull/127054))
- Parse const trait bounds with bound lifetimes
([#&#8203;1902](https://redirect.github.com/dtolnay/syn/issues/1902))
- Parse bound lifetimes with lifetime bounds
([#&#8203;1903](https://redirect.github.com/dtolnay/syn/issues/1903))
- Allow type parameters and const parameters in trait bounds and generic
closures
([#&#8203;1904](https://redirect.github.com/dtolnay/syn/issues/1904),
[#&#8203;1907](https://redirect.github.com/dtolnay/syn/issues/1907),
[#&#8203;1908](https://redirect.github.com/dtolnay/syn/issues/1908),
[#&#8203;1909](https://redirect.github.com/dtolnay/syn/issues/1909))

###
[`v2.0.105`](https://redirect.github.com/dtolnay/syn/releases/tag/2.0.105)

[Compare
Source](https://redirect.github.com/dtolnay/syn/compare/2.0.104...2.0.105)

- Disallow "negative" inherent impls like `impl !T {}`
([#&#8203;1881](https://redirect.github.com/dtolnay/syn/issues/1881),
[rust-lang/rust#144386](https://redirect.github.com/rust-lang/rust/pull/144386))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:41:48 +05:30
renovate[bot]
a0bba718f6 Update Rust crate tracing-indicatif to v0.3.13 (#20072)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[tracing-indicatif](https://redirect.github.com/emersonford/tracing-indicatif)
| workspace.dependencies | patch | `0.3.12` -> `0.3.13` |

---

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

---

### Release Notes

<details>
<summary>emersonford/tracing-indicatif (tracing-indicatif)</summary>

###
[`v0.3.13`](https://redirect.github.com/emersonford/tracing-indicatif/blob/HEAD/CHANGELOG.md#0313---2025-08-15)

[Compare
Source](https://redirect.github.com/emersonford/tracing-indicatif/compare/0.3.12...0.3.13)

- eliminate panics on internal lock poison

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:41:33 +05:30
renovate[bot]
1eab4dbd95 Update Rust crate thiserror to v2.0.16 (#20071)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [thiserror](https://redirect.github.com/dtolnay/thiserror) |
workspace.dependencies | patch | `2.0.12` -> `2.0.16` |

---

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

---

### Release Notes

<details>
<summary>dtolnay/thiserror (thiserror)</summary>

###
[`v2.0.16`](https://redirect.github.com/dtolnay/thiserror/releases/tag/2.0.16)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/2.0.15...2.0.16)

- Add to "no-std" crates.io category
([#&#8203;429](https://redirect.github.com/dtolnay/thiserror/issues/429))

###
[`v2.0.15`](https://redirect.github.com/dtolnay/thiserror/releases/tag/2.0.15)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/2.0.14...2.0.15)

- Prevent `Error::provide` API becoming unavailable from a future new
compiler lint
([#&#8203;427](https://redirect.github.com/dtolnay/thiserror/issues/427))

###
[`v2.0.14`](https://redirect.github.com/dtolnay/thiserror/releases/tag/2.0.14)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/2.0.13...2.0.14)

- Allow build-script cleanup failure with NFSv3 output directory to be
non-fatal
([#&#8203;426](https://redirect.github.com/dtolnay/thiserror/issues/426))

###
[`v2.0.13`](https://redirect.github.com/dtolnay/thiserror/releases/tag/2.0.13)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/2.0.12...2.0.13)

- Documentation improvements

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:41:18 +05:30
renovate[bot]
3eb3c3572b Update astral-sh/setup-uv action to v6.6.0 (#20074)
This PR contains the following updates:

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

---

> [!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.0`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.6.0):
🌈 Support for .tools-versions

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

##### Changes

This release adds support for [asdf](https://asdf-vm.com/)
`.tool-versions` in the `version-file` input

##### 🐛 Bug fixes

- Add log message before long API calls to GitHub
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;530](https://redirect.github.com/astral-sh/setup-uv/issues/530))

##### 🚀 Enhancements

- Add support for .tools-versions
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;531](https://redirect.github.com/astral-sh/setup-uv/issues/531))

##### 🧰 Maintenance

- Bump dependencies
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;532](https://redirect.github.com/astral-sh/setup-uv/issues/532))
- chore: update known versions for 0.8.12
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;529](https://redirect.github.com/astral-sh/setup-uv/issues/529))
- chore: update known versions for 0.8.11
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;526](https://redirect.github.com/astral-sh/setup-uv/issues/526))
- chore: update known versions for 0.8.10
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;525](https://redirect.github.com/astral-sh/setup-uv/issues/525))

###
[`v6.5.0`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.5.0):
🌈 Better error messages, bug fixes and copilot agent settings

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

##### Changes

This release brings better error messages in case the GitHub API is
impacted, fixes a few bugs and allows to disable [problem
matchers](https://redirect.github.com/actions/toolkit/blob/main/docs/problem-matchers.md)
for better use in Copilot Agent workspaces.

##### 🐛 Bug fixes

- Improve error messages on GitHub API errors
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;518](https://redirect.github.com/astral-sh/setup-uv/issues/518))
- Ignore backslashes and whitespace in requirements
[@&#8203;axm2](https://redirect.github.com/axm2)
([#&#8203;501](https://redirect.github.com/astral-sh/setup-uv/issues/501))

##### 🚀 Enhancements

- Add input add-problem-matchers
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;517](https://redirect.github.com/astral-sh/setup-uv/issues/517))

##### 🧰 Maintenance

- chore: update known versions for 0.8.9
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;512](https://redirect.github.com/astral-sh/setup-uv/issues/512))
- chore: update known versions for 0.8.6-0.8.8
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;510](https://redirect.github.com/astral-sh/setup-uv/issues/510))
- chore: update known versions for 0.8.5
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;509](https://redirect.github.com/astral-sh/setup-uv/issues/509))
- chore: update known versions for 0.8.4
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;505](https://redirect.github.com/astral-sh/setup-uv/issues/505))
- chore: update known versions for 0.8.3
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;502](https://redirect.github.com/astral-sh/setup-uv/issues/502))

##### 📚 Documentation

- add note on caching to read disable-cache-pruning
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;506](https://redirect.github.com/astral-sh/setup-uv/issues/506))

##### ⬆️ Dependency updates

- Bump actions/checkout from 4 to 5
@&#8203;[dependabot\[bot\]](https://redirect.github.com/apps/dependabot)
([#&#8203;514](https://redirect.github.com/astral-sh/setup-uv/issues/514))
- bump dependencies
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;516](https://redirect.github.com/astral-sh/setup-uv/issues/516))
- Bump biome to v2
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;515](https://redirect.github.com/astral-sh/setup-uv/issues/515))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:40:46 +05:30
renovate[bot]
41bb24a87e Update Rust crate serde_json to v1.0.143 (#20069) 2025-08-24 22:20:39 -04:00
renovate[bot]
87f0da139a Update Rust crate proc-macro2 to v1.0.101 (#20066) 2025-08-24 22:20:24 -04:00
renovate[bot]
f9bfc9ab5b Update Rust crate ordermap to v0.5.9 (#20065) 2025-08-24 22:20:17 -04:00
renovate[bot]
59f7102606 Update Rust crate bitflags to v2.9.3 (#20063) 2025-08-24 22:19:54 -04:00
renovate[bot]
862d2d0687 Update dependency ruff to v0.12.10 (#20062) 2025-08-24 22:19:44 -04:00
renovate[bot]
48edf46f3b Update Rust crate filetime to v0.2.26 (#20064) 2025-08-24 22:19:34 -04:00
Jelle Zijlstra
ec86a4e960 [ty] Add Top[] and Bottom[] special forms, replacing top_materialization_of() function (#20054)
Part of astral-sh/ty#994

## Summary

Add new special forms to `ty_extensions`, `Top[T]` and `Bottom[T]`.
Remove `ty_extensions.top_materialization` and
`ty_extensions.bottom_materialization`.

## Test Plan

Converted the existing `materialization.md` mdtest to the new syntax.
Added some tests for invalid use of the new special form.
2025-08-23 11:20:56 -07:00
Andrew Gallant
e7237652a9 [ty] Lightly refactor document symbols AST visitor
This makes use of early continue/return to keep rightward drift under
control. (I also find it easier to read.)
2025-08-23 12:53:41 -04:00
Andrew Gallant
205eae14d2 [ty] Rejigger workspace symbols for more efficient caching
In effect, we make the Salsa query aspect keyed only on whether we want
global symbols. We move everything else (hierarchical and querying) to
an aggregate step *after* the query.

This was a somewhat involved change since we want to return a flattened
list from visiting the source while also preserving enough information
to reform the symbols into a hierarchical structure that the LSP
expects. But I think overall the API has gotten simpler and we encode
more invariants into the type system. (For example, previously you got a
runtime assertion if you tried to provide a query string while enabling
hierarchical mode. But now that's prevented by construction.)
2025-08-23 12:53:41 -04:00
Andrew Gallant
f407f12f4c [ty] Parallelize workspace symbols
This is a pretty naive approach, but it makes cold times for listing
workspace symbols in home-assistant under 1s on my machine.

Courtesy of Micha:
https://github.com/astral-sh/ruff/pull/20030#discussion_r2292924171
2025-08-23 12:53:41 -04:00
Andrew Gallant
fb2d0af18c [ty] Optimize "workspace symbols" retrieval
Basically, this splits the implementation into two pieces:
the first piece does the traversal and finds *all* symbols
across the workspace. The second piece does filtering based
on a user provided query string. Only the first piece is
cached by Salsa.

This brings warm "workspace symbols" requests down from
500-600ms to 100-200ms.
2025-08-23 12:53:41 -04:00
Andrew Gallant
8ead02e0b1 [ty] Optimize query string matching
While this doesn't typically matter, when ty returns a very
large list of symbols, this can have an impact. Specifically,
when searching `async` in home-assistant, this gets times
closer to 500ms versus closer to 600ms before this change.
It looks like an overall ~50ms improvement (so around 10%),
but variance is all over the place and I didn't do any
statistical tests.

But this does make intuitive sense. Previously, we were
allocating intermediate strings, doing UTF-8 decoding and
consulting Unicode casing tables. Now we're just doing what
is likely a single DFA scan. In effect, we front load all
of the Unicode junk into regex compilation.
2025-08-23 12:53:41 -04:00
Andrew Gallant
330bb4efbf [ty] Add some unit tests for "query matches symbol"
There is a small amount of subtlety to this matching routine,
and it could be implemented in a faster way. So let's right some
tests for what we have to ensure we don't break anything when
we optimize it.
2025-08-23 12:53:41 -04:00
Andrew Gallant
ad8c98117a [ty] Move query filtering outside of symbol visitor
This is prep work for turning this into a Salsa query.
Specifically, we don't want the Salsa query to be
dependent upon the query string.
2025-08-23 12:53:41 -04:00
Andrew Gallant
06dbec8479 [ty] Add debug trace for workspace symbol elapsed time
Useful for ad hoc debugging, but it's also useful to have permanently to
enable serendipitous discovery of performance problems.
2025-08-23 12:53:41 -04:00
Andrew Gallant
85931ab594 [ty] Add a TODO for linting on todo! 2025-08-23 12:53:41 -04:00
Ibraheem Ahmed
b3cc733f06 [ty] Remove duplicate global lint registry (#20053)
## Summary

Looks like an oversight at some point that led to two identical globals,
the one in `ty_project` just calls `ty_python_semantic::register_lints`.
2025-08-22 19:43:12 -04:00
325 changed files with 10206 additions and 5019 deletions

View File

@@ -441,7 +441,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@79e4beb1e02f733a26129a6bf26c37dab4ab3307 # v1.14.4
uses: cargo-bins/cargo-binstall@0dca8cf8dfb40cb77a29cece06933ce674674523 # v1.15.1
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -463,7 +463,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
@@ -664,7 +664,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -694,7 +694,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@79e4beb1e02f733a26129a6bf26c37dab4ab3307 # v1.14.4
- uses: cargo-bins/cargo-binstall@0dca8cf8dfb40cb77a29cece06933ce674674523 # v1.15.1
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -734,7 +734,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
@@ -777,7 +777,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -909,7 +909,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: "Install Rust toolchain"
run: rustup show
@@ -942,7 +942,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- 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@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- 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@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- 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@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- 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@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels-*

View File

@@ -61,7 +61,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
@@ -124,7 +124,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
@@ -175,7 +175,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
@@ -251,7 +251,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive

View File

@@ -65,7 +65,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- 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@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- 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@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- 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@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

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

View File

@@ -1,5 +1,32 @@
# Changelog
## 0.12.11
### Preview features
- \[`airflow`\] Extend `AIR311` and `AIR312` rules ([#20082](https://github.com/astral-sh/ruff/pull/20082))
- \[`airflow`\] Replace wrong path `airflow.io.storage` with `airflow.io.store` (`AIR311`) ([#20081](https://github.com/astral-sh/ruff/pull/20081))
- \[`flake8-async`\] Implement `blocking-http-call-httpx-in-async-function` (`ASYNC212`) ([#20091](https://github.com/astral-sh/ruff/pull/20091))
- \[`flake8-logging-format`\] Add auto-fix for f-string logging calls (`G004`) ([#19303](https://github.com/astral-sh/ruff/pull/19303))
- \[`flake8-use-pathlib`\] Add autofix for `PTH211` ([#20009](https://github.com/astral-sh/ruff/pull/20009))
- \[`flake8-use-pathlib`\] Make `PTH100` fix unsafe because it can change behavior ([#20100](https://github.com/astral-sh/ruff/pull/20100))
### Bug fixes
- \[`pyflakes`, `pylint`\] Fix false positives caused by `__class__` cell handling (`F841`, `PLE0117`) ([#20048](https://github.com/astral-sh/ruff/pull/20048))
- \[`pyflakes`\] Fix `allowed-unused-imports` matching for top-level modules (`F401`) ([#20115](https://github.com/astral-sh/ruff/pull/20115))
- \[`ruff`\] Fix false positive for t-strings in `default-factory-kwarg` (`RUF026`) ([#20032](https://github.com/astral-sh/ruff/pull/20032))
- \[`ruff`\] Preserve relative whitespace in multi-line expressions (`RUF033`) ([#19647](https://github.com/astral-sh/ruff/pull/19647))
### Rule changes
- \[`ruff`\] Handle empty t-strings in `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#20045](https://github.com/astral-sh/ruff/pull/20045))
### Documentation
- Fix incorrect `D413` links in docstrings convention FAQ ([#20089](https://github.com/astral-sh/ruff/pull/20089))
- \[`flake8-use-pathlib`\] Update links to the table showing the correspondence between `os` and `pathlib` ([#20103](https://github.com/astral-sh/ruff/pull/20103))
## 0.12.10
### Preview features

207
Cargo.lock generated
View File

@@ -257,12 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
dependencies = [
"serde",
]
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
[[package]]
name = "bitvec"
@@ -298,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.9",
"regex-automata 0.4.10",
"serde",
]
@@ -488,7 +485,7 @@ checksum = "85a8ab73a1c02b0c15597b22e09c7dc36e63b2f601f9d1e83ac0c3decd38b1ae"
dependencies = [
"nix 0.29.0",
"terminfo",
"thiserror 2.0.12",
"thiserror 2.0.16",
"which",
"windows-sys 0.59.0",
]
@@ -958,7 +955,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -1031,16 +1028,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
dependencies = [
"serde",
"typeid",
]
[[package]]
name = "errno"
version = "0.3.13"
@@ -1048,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.59.0",
]
[[package]]
@@ -1097,14 +1084,14 @@ dependencies = [
[[package]]
name = "filetime"
version = "0.2.25"
version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -1131,9 +1118,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
@@ -1244,7 +1231,7 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-automata 0.4.10",
"regex-syntax 0.8.5",
]
@@ -1254,7 +1241,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"ignore",
"walkdir",
]
@@ -1443,9 +1430,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
@@ -1472,7 +1459,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata 0.4.9",
"regex-automata 0.4.10",
"same-file",
"walkdir",
"winapi-util",
@@ -1499,9 +1486,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
@@ -1534,7 +1521,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"inotify-sys",
"libc",
]
@@ -1793,7 +1780,7 @@ dependencies = [
"paste",
"peg",
"regex",
"thiserror 2.0.12",
"thiserror 2.0.16",
]
[[package]]
@@ -1822,7 +1809,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"libc",
"redox_syscall",
]
@@ -2027,7 +2014,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2039,7 +2026,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2067,7 +2054,7 @@ version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"fsevent-sys",
"inotify",
"kqueue",
@@ -2140,9 +2127,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6bff06e4a5dc6416bead102d3e63c480dd852ffbb278bf8cfeb4966b329609"
checksum = "2fd6fedcd996c8c97932075cc3811d83f53280f48d5620e4e3cab7f6a12678c4"
dependencies = [
"indexmap",
"serde",
@@ -2296,9 +2283,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
@@ -2307,7 +2294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
"thiserror 2.0.12",
"thiserror 2.0.16",
"ucd-trie",
]
@@ -2486,9 +2473,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.96"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
@@ -2503,7 +2490,7 @@ dependencies = [
"pep440_rs",
"pep508_rs",
"serde",
"thiserror 2.0.12",
"thiserror 2.0.16",
"toml 0.8.23",
]
@@ -2518,7 +2505,7 @@ dependencies = [
"newtype-uuid",
"quick-xml",
"strip-ansi-escapes",
"thiserror 2.0.12",
"thiserror 2.0.16",
"uuid",
]
@@ -2679,7 +2666,7 @@ version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
]
[[package]]
@@ -2690,18 +2677,18 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.12",
"thiserror 2.0.16",
]
[[package]]
name = "regex"
version = "1.11.1"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-automata 0.4.10",
"regex-syntax 0.8.5",
]
@@ -2716,9 +2703,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
@@ -2756,13 +2743,13 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.10"
version = "0.12.11"
dependencies = [
"anyhow",
"argfile",
"assert_fs",
"bincode 2.0.1",
"bitflags 2.9.2",
"bitflags 2.9.3",
"cachedir",
"clap",
"clap_complete_command",
@@ -2806,7 +2793,7 @@ dependencies = [
"strum",
"tempfile",
"test-case",
"thiserror 2.0.12",
"thiserror 2.0.16",
"tikv-jemallocator",
"toml 0.9.5",
"tracing",
@@ -2883,6 +2870,7 @@ dependencies = [
"insta",
"matchit",
"path-slash",
"pathdiff",
"quick-junit",
"ruff_annotate_snippets",
"ruff_cache",
@@ -2901,7 +2889,7 @@ dependencies = [
"serde_json",
"similar",
"tempfile",
"thiserror 2.0.12",
"thiserror 2.0.16",
"tracing",
"tracing-subscriber",
"ty_static",
@@ -2948,6 +2936,7 @@ dependencies = [
"tracing-subscriber",
"ty",
"ty_project",
"ty_python_semantic",
"ty_static",
"url",
]
@@ -3010,11 +2999,11 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.10"
version = "0.12.11"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.9.2",
"bitflags 2.9.3",
"clap",
"colored 3.0.0",
"fern",
@@ -3032,7 +3021,6 @@ dependencies = [
"memchr",
"natord",
"path-absolutize",
"pathdiff",
"pep440_rs",
"pyproject-toml",
"regex",
@@ -3061,7 +3049,7 @@ dependencies = [
"strum_macros",
"tempfile",
"test-case",
"thiserror 2.0.12",
"thiserror 2.0.16",
"toml 0.9.5",
"typed-arena",
"unicode-normalization",
@@ -3104,7 +3092,7 @@ dependencies = [
"serde_json",
"serde_with",
"test-case",
"thiserror 2.0.12",
"thiserror 2.0.16",
"uuid",
]
@@ -3120,7 +3108,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.9.2",
"bitflags 2.9.3",
"compact_str",
"get-size2",
"is-macro",
@@ -3135,7 +3123,7 @@ dependencies = [
"salsa",
"schemars",
"serde",
"thiserror 2.0.12",
"thiserror 2.0.16",
]
[[package]]
@@ -3189,7 +3177,7 @@ dependencies = [
"similar",
"smallvec",
"static_assertions",
"thiserror 2.0.12",
"thiserror 2.0.16",
"tracing",
]
@@ -3208,7 +3196,7 @@ dependencies = [
name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"itertools 0.14.0",
"ruff_python_ast",
"unic-ucd-category",
@@ -3219,7 +3207,7 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.2",
"bitflags 2.9.3",
"bstr",
"compact_str",
"get-size2",
@@ -3244,7 +3232,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"insta",
"is-macro",
"ruff_cache",
@@ -3265,7 +3253,7 @@ dependencies = [
name = "ruff_python_stdlib"
version = "0.0.0"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"unicode-ident",
]
@@ -3319,7 +3307,7 @@ dependencies = [
"serde",
"serde_json",
"shellexpand",
"thiserror 2.0.12",
"thiserror 2.0.16",
"toml 0.9.5",
"tracing",
"tracing-log",
@@ -3349,7 +3337,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.10"
version = "0.12.11"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3442,11 +3430,11 @@ version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -3464,13 +3452,12 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a0e7a06#a0e7a0660c93136f23bf08b4f1604eee3d1f6b11"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
dependencies = [
"boxcar",
"compact_str",
"crossbeam-queue",
"crossbeam-utils",
"erased-serde",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
@@ -3481,7 +3468,6 @@ dependencies = [
"rustc-hash",
"salsa-macro-rules",
"salsa-macros",
"serde",
"smallvec",
"thin-vec",
"tracing",
@@ -3490,12 +3476,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a0e7a06#a0e7a0660c93136f23bf08b4f1604eee3d1f6b11"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
[[package]]
name = "salsa-macros"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a0e7a06#a0e7a0660c93136f23bf08b4f1604eee3d1f6b11"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
dependencies = [
"proc-macro2",
"quote",
@@ -3592,9 +3578,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.142"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"itoa",
"memchr",
@@ -3715,9 +3701,6 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "snapbox"
@@ -3808,9 +3791,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@@ -3922,9 +3905,6 @@ name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
dependencies = [
"serde",
]
[[package]]
name = "thiserror"
@@ -3937,11 +3917,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.12"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
"thiserror-impl 2.0.12",
"thiserror-impl 2.0.16",
]
[[package]]
@@ -3957,9 +3937,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.12"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
@@ -4158,9 +4138,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
version = "0.3.12"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1983afead46ff13a3c93581e0cec31d20b29efdd22cbdaa8b9f850eccf2c352"
checksum = "04d4e11e0e27acef25a47f27e9435355fecdc488867fa2bc90e75b0700d2823d"
dependencies = [
"indicatif",
"tracing",
@@ -4260,17 +4240,22 @@ dependencies = [
name = "ty_ide"
version = "0.0.0"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"get-size2",
"insta",
"itertools 0.14.0",
"rayon",
"regex",
"ruff_db",
"ruff_index",
"ruff_memory_usage",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash",
"salsa",
"smallvec",
"tracing",
"ty_project",
@@ -4282,7 +4267,6 @@ name = "ty_project"
version = "0.0.0"
dependencies = [
"anyhow",
"bincode 2.0.1",
"camino",
"colored 3.0.0",
"crossbeam",
@@ -4294,7 +4278,7 @@ dependencies = [
"pep440_rs",
"rayon",
"regex",
"regex-automata 0.4.9",
"regex-automata 0.4.10",
"ruff_cache",
"ruff_db",
"ruff_macros",
@@ -4307,12 +4291,11 @@ dependencies = [
"salsa",
"schemars",
"serde",
"thiserror 2.0.12",
"thiserror 2.0.16",
"toml 0.9.5",
"tracing",
"ty_combine",
"ty_python_semantic",
"ty_static",
"ty_vendored",
]
@@ -4321,7 +4304,7 @@ name = "ty_python_semantic"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.2",
"bitflags 2.9.3",
"bitvec",
"camino",
"colored 3.0.0",
@@ -4361,7 +4344,7 @@ dependencies = [
"strum_macros",
"tempfile",
"test-case",
"thiserror 2.0.12",
"thiserror 2.0.16",
"tracing",
"ty_python_semantic",
"ty_static",
@@ -4374,7 +4357,7 @@ name = "ty_server"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.2",
"bitflags 2.9.3",
"crossbeam",
"dunce",
"insta",
@@ -4395,7 +4378,7 @@ dependencies = [
"serde_json",
"shellexpand",
"tempfile",
"thiserror 2.0.12",
"thiserror 2.0.16",
"tracing",
"tracing-subscriber",
"ty_combine",
@@ -4417,7 +4400,7 @@ name = "ty_test"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.2",
"bitflags 2.9.3",
"camino",
"colored 3.0.0",
"insta",
@@ -4436,7 +4419,7 @@ dependencies = [
"serde",
"smallvec",
"tempfile",
"thiserror 2.0.12",
"thiserror 2.0.16",
"toml 0.9.5",
"tracing",
"ty_python_semantic",
@@ -4484,12 +4467,6 @@ version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.18.0"
@@ -4619,9 +4596,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.4"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
@@ -5173,7 +5150,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
]
[[package]]

View File

@@ -57,8 +57,8 @@ anyhow = { version = "1.0.80" }
arc-swap = { version = "1.7.1" }
assert_fs = { version = "1.1.0" }
argfile = { version = "0.2.0" }
bincode = { version = "2.0.0", features = ["serde"] }
bitflags = { version = "2.5.0", features = ["serde"] }
bincode = { version = "2.0.0" }
bitflags = { version = "2.5.0" }
bitvec = { version = "1.0.1", default-features = false, features = [
"alloc",
] }
@@ -126,7 +126,7 @@ memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "8.0.0" }
ordermap = { version = "0.5.0", features = ["serde"] }
ordermap = { version = "0.5.0" }
path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" }
@@ -143,25 +143,24 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a0e7a06", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
"inventory",
"persistence",
] }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive", "rc"] }
serde = { version = "1.0.197", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.4" }
serde_json = { version = "1.0.142" }
serde_json = { version = "1.0.113" }
serde_test = { version = "1.0.152" }
serde_with = { version = "3.6.0", default-features = false, features = [
"macros",
] }
shellexpand = { version = "3.0.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new", "serde"] }
smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new"] }
snapbox = { version = "0.6.0", features = [
"diff",
"term-svg",

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.12.10"
version = "0.12.11"
publish = true
authors = { workspace = true }
edition = { workspace = true }
@@ -31,7 +31,7 @@ ruff_workspace = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
bincode = { workspace = true }
bincode = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
cachedir = { workspace = true }
clap = { workspace = true, features = ["derive", "env", "wrap_help"] }

View File

@@ -15,8 +15,7 @@ use ruff_db::diagnostic::{
use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{
Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, SarifEmitter,
TextEmitter,
Emitter, EmitterContext, GithubEmitter, GroupedEmitter, SarifEmitter, TextEmitter,
};
use ruff_linter::notify_user;
use ruff_linter::settings::flags::{self};
@@ -296,7 +295,11 @@ impl Printer {
GithubEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Gitlab => {
GitlabEmitter::default().emit(writer, &diagnostics.inner, &context)?;
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Gitlab)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Pylint => {
let config = DisplayDiagnosticConfig::default()

View File

@@ -20,59 +20,59 @@ exit_code: 1
{
"check_name": "F401",
"description": "F401: `os` imported but unused",
"severity": "major",
"fingerprint": "4dbad37161e65c72",
"location": {
"path": "input.py",
"positions": {
"begin": {
"column": 8,
"line": 1
"line": 1,
"column": 8
},
"end": {
"column": 10,
"line": 1
"line": 1,
"column": 10
}
}
},
"severity": "major"
}
},
{
"check_name": "F821",
"description": "F821: Undefined name `y`",
"severity": "major",
"fingerprint": "7af59862a085230",
"location": {
"path": "input.py",
"positions": {
"begin": {
"column": 5,
"line": 2
"line": 2,
"column": 5
},
"end": {
"column": 6,
"line": 2
"line": 2,
"column": 6
}
}
},
"severity": "major"
}
},
{
"check_name": "invalid-syntax",
"description": "invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"severity": "major",
"fingerprint": "e558cec859bb66e8",
"location": {
"path": "input.py",
"positions": {
"begin": {
"column": 1,
"line": 3
"line": 3,
"column": 1
},
"end": {
"column": 6,
"line": 3
"line": 3,
"column": 6
}
}
},
"severity": "major"
}
}
]
----- stderr -----

View File

@@ -450,9 +450,6 @@ fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) {
r#"
class C:
def f(self: "C"):
self.a = ""
self.b = ""
if isinstance(self.a, str):
return
@@ -466,6 +463,56 @@ fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) {
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
self.a = ""
self.b = ""
"#,
)
},
|case| {
let Case { db, .. } = case;
let result = db.check();
assert_eq!(result.len(), 0);
},
BatchSize::SmallInput,
);
});
}
fn benchmark_complex_constrained_attributes_3(criterion: &mut Criterion) {
setup_rayon();
criterion.bench_function("ty_micro[complex_constrained_attributes_3]", |b| {
b.iter_batched_ref(
|| {
// This is a regression test for https://github.com/astral-sh/ty/issues/758
setup_micro_case(
r#"
class GridOut:
def __init__(self: "GridOut") -> None:
self._buffer = b""
def _read_size_or_line(self: "GridOut", size: int = -1):
if size > self._position:
size = self._position
pass
if size == 0:
return bytes()
while size > 0:
if self._buffer:
buf = self._buffer
self._buffer = b""
else:
buf = b""
if len(buf) > size:
self._buffer = buf
self._position -= len(self._buffer)
"#,
)
},
@@ -668,6 +715,7 @@ criterion_group!(
benchmark_tuple_implicit_instance_attributes,
benchmark_complex_constrained_attributes_1,
benchmark_complex_constrained_attributes_2,
benchmark_complex_constrained_attributes_3,
benchmark_many_enum_members,
);
criterion_group!(project, anyio, attrs, hydra, datetype);

View File

@@ -34,6 +34,7 @@ glob = { workspace = true }
ignore = { workspace = true, optional = true }
matchit = { workspace = true }
path-slash = { workspace = true }
pathdiff = { workspace = true }
quick-junit = { workspace = true, optional = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
@@ -53,7 +54,7 @@ web-time = { version = "1.1.0" }
etcetera = { workspace = true, optional = true }
[dev-dependencies]
insta = { workspace = true }
insta = { workspace = true, features = ["filters"] }
tempfile = { workspace = true }
[features]

View File

@@ -22,7 +22,6 @@ mod stylesheet;
/// a characteristic is a deficiency. An example of a characteristic that is
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Diagnostic {
/// The actual diagnostic.
///
@@ -326,6 +325,11 @@ impl Diagnostic {
self.inner.fix.as_ref()
}
#[cfg(test)]
pub(crate) fn fix_mut(&mut self) -> Option<&mut Fix> {
Arc::make_mut(&mut self.inner).fix.as_mut()
}
/// Set the fix for this diagnostic.
pub fn set_fix(&mut self, fix: Fix) {
debug_assert!(
@@ -501,7 +505,6 @@ impl Diagnostic {
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct DiagnosticInner {
id: DiagnosticId,
severity: Severity,
@@ -578,7 +581,6 @@ impl Eq for RenderingSortKey<'_> {}
/// another (for a single parent diagnostic) is the order in which they were
/// attached to the diagnostic.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct SubDiagnostic {
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
/// pointer-sized.
@@ -688,7 +690,6 @@ impl SubDiagnostic {
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct SubDiagnosticInner {
severity: SubDiagnosticSeverity,
message: DiagnosticMessage,
@@ -717,7 +718,6 @@ struct SubDiagnosticInner {
/// Messages attached to annotations should also be as brief and specific as
/// possible. Long messages could negative impact the quality of rendering.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Annotation {
/// The span of this annotation, corresponding to some subsequence of the
/// user's input that we want to highlight.
@@ -860,7 +860,6 @@ impl Annotation {
/// These tags are used to provide additional information about the annotation.
/// and are passed through to the language server protocol.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum DiagnosticTag {
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
Unnecessary,
@@ -875,7 +874,6 @@ pub enum DiagnosticTag {
///
/// Rules use kebab case, e.g. `no-foo`.
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
pub struct LintName(&'static str);
impl LintName {
@@ -888,66 +886,6 @@ impl LintName {
}
}
#[cfg(feature = "serde")]
pub use lint_name_serde::LintRegistryGuard;
#[cfg(feature = "serde")]
mod lint_name_serde {
use super::LintName;
use std::cell::RefCell;
thread_local! {
/// Serde doesn't provide any easy means to pass a value to a [`Deserialize`] implementation,
/// but we need a way to retrieve static [`LintName`]s from the lint registry when deserializing.
///
/// Use the [`LintRegistryGuard`] to initialize the thread local before calling into any
/// deserialization code. It ensures that the thread local variable gets cleaned up
/// once deserialization is done (once the guard gets dropped).
static LINT_REGISTRY: RefCell<Option<LintRegistry>> = const { RefCell::new(None) };
}
type LintRegistry = fn(&str) -> Option<LintName>;
/// Guard to safely change the lint registry for the current thread.
#[must_use]
pub struct LintRegistryGuard {
prev_value: Option<LintRegistry>,
}
impl LintRegistryGuard {
pub fn new(registry: LintRegistry) -> Self {
let prev = LINT_REGISTRY.replace(Some(registry));
Self { prev_value: prev }
}
}
impl Drop for LintRegistryGuard {
fn drop(&mut self) {
LINT_REGISTRY.set(self.prev_value.take());
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for LintName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let name: &str = serde::Deserialize::deserialize(deserializer)?;
LINT_REGISTRY.with_borrow(|registry| {
let registry = registry
.expect("must set the `LintRegistryGuard` when deserializing a `LintName`");
registry(name).ok_or(serde::de::Error::custom(format!(
"invalid `LintName` {name}"
)))
})
}
}
}
impl std::ops::Deref for LintName {
type Target = str;
@@ -976,7 +914,6 @@ impl PartialEq<&str> for LintName {
/// Uniquely identifies the kind of a diagnostic.
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum DiagnosticId {
Panic,
@@ -1165,30 +1102,6 @@ impl UnifiedFile {
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for UnifiedFile {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
UnifiedFile::Ty(file) => serde::Serialize::serialize(file, serializer),
// Persistent caching is only used in ty.
UnifiedFile::Ruff(..) => panic!("Ruff files are not persistable"),
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for UnifiedFile {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
serde::Deserialize::deserialize(deserializer).map(UnifiedFile::Ty)
}
}
/// A unified wrapper for types that can be converted to a [`SourceCode`].
///
/// As with [`UnifiedFile`], ruff and ty use slightly different representations for source code.
@@ -1220,7 +1133,6 @@ impl DiagnosticSource {
/// range isn't present, it semantically implies that the diagnostic refers to
/// the entire file. For example, when the file should be executable but isn't.
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Span {
file: UnifiedFile,
range: Option<TextRange>,
@@ -1299,7 +1211,6 @@ impl From<crate::files::FileRange> for Span {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Severity {
Info,
Warning,
@@ -1335,7 +1246,6 @@ impl Severity {
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
/// deleted and the two combined again.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum SubDiagnosticSeverity {
Help,
Info,
@@ -1525,6 +1435,11 @@ pub enum DiagnosticFormat {
/// Print diagnostics in the format expected by JUnit.
#[cfg(feature = "junit")]
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
#[cfg(feature = "serde")]
Gitlab,
}
/// A representation of the kinds of messages inside a diagnostic.
@@ -1584,7 +1499,6 @@ impl std::fmt::Display for ConciseMessage<'_> {
/// a blanket trait implementation for `IntoDiagnosticMessage` for
/// anything that implements `std::fmt::Display`.
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DiagnosticMessage(Box<str>);
impl DiagnosticMessage {
@@ -1648,11 +1562,7 @@ impl<T: std::fmt::Display> IntoDiagnosticMessage for T {
///
/// For Ruff rules this means the noqa code.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(transparent)
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
pub struct SecondaryCode(String);
impl SecondaryCode {

View File

@@ -31,6 +31,8 @@ mod azure;
mod concise;
mod full;
#[cfg(feature = "serde")]
mod gitlab;
#[cfg(feature = "serde")]
mod json;
#[cfg(feature = "serde")]
mod json_lines;
@@ -136,6 +138,10 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
DiagnosticFormat::Junit => {
junit::JunitRenderer::new(self.resolver).render(f, self.diagnostics)?;
}
#[cfg(feature = "serde")]
DiagnosticFormat::Gitlab => {
gitlab::GitlabRenderer::new(self.resolver).render(f, self.diagnostics)?;
}
}
Ok(())
@@ -2622,6 +2628,13 @@ watermelon
self.config = config;
}
/// Show a diff for the fix when rendering.
pub(super) fn show_fix_diff(&mut self, yes: bool) {
let mut config = std::mem::take(&mut self.config);
config = config.show_fix_diff(yes);
self.config = config;
}
/// The lowest fix applicability to show when rendering.
pub(super) fn fix_applicability(&mut self, applicability: Applicability) {
let mut config = std::mem::take(&mut self.config);

View File

@@ -2,12 +2,13 @@ use std::borrow::Cow;
use std::num::NonZeroUsize;
use anstyle::Style;
use ruff_notebook::NotebookIndex;
use similar::{ChangeTag, TextDiff};
use ruff_annotate_snippets::Renderer as AnnotateRenderer;
use ruff_diagnostics::{Applicability, Fix};
use ruff_source_file::OneIndexed;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::diagnostic::render::{FileResolver, Resolved};
use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled};
@@ -81,6 +82,7 @@ impl<'a> FullRenderer<'a> {
struct Diff<'a> {
fix: &'a Fix,
diagnostic_source: DiagnosticSource,
notebook_index: Option<NotebookIndex>,
stylesheet: &'a DiagnosticStylesheet,
}
@@ -90,12 +92,11 @@ impl<'a> Diff<'a> {
stylesheet: &'a DiagnosticStylesheet,
resolver: &'a dyn FileResolver,
) -> Option<Diff<'a>> {
let file = &diagnostic.primary_span_ref()?.file;
Some(Diff {
fix: diagnostic.fix()?,
diagnostic_source: diagnostic
.primary_span_ref()?
.file
.diagnostic_source(resolver),
diagnostic_source: file.diagnostic_source(resolver),
notebook_index: resolver.notebook_index(file),
stylesheet,
})
}
@@ -106,19 +107,24 @@ impl std::fmt::Display for Diff<'_> {
let source_code = self.diagnostic_source.as_source_code();
let source_text = source_code.text();
// TODO(dhruvmanila): Add support for Notebook cells once it's user-facing
let mut output = String::with_capacity(source_text.len());
let mut last_end = TextSize::default();
for edit in self.fix.edits() {
output.push_str(source_code.slice(TextRange::new(last_end, edit.start())));
output.push_str(edit.content().unwrap_or_default());
last_end = edit.end();
}
output.push_str(&source_text[usize::from(last_end)..]);
let diff = TextDiff::from_lines(source_text, &output);
// Partition the source code into end offsets for each cell. If `self.notebook_index` is
// `None`, indicating a regular script file, all the lines will be in one "cell" under the
// `None` key.
let cells = if let Some(notebook_index) = &self.notebook_index {
let mut last_cell = OneIndexed::MIN;
let mut cells: Vec<(Option<OneIndexed>, TextSize)> = Vec::new();
for (row, cell) in notebook_index.iter() {
if cell != last_cell {
let offset = source_code.line_start(row);
cells.push((Some(last_cell), offset));
last_cell = cell;
}
}
cells.push((Some(last_cell), source_text.text_len()));
cells
} else {
vec![(None, source_text.text_len())]
};
let message = match self.fix.applicability() {
// TODO(zanieb): Adjust this messaging once it's user-facing
@@ -133,59 +139,97 @@ impl std::fmt::Display for Diff<'_> {
// tests, which is the only place these are currently used.
writeln!(f, " {}", fmt_styled(message, self.stylesheet.separator))?;
let (largest_old, largest_new) = diff
.ops()
.last()
.map(|op| (op.old_range().start, op.new_range().start))
.unwrap_or_default();
let mut last_end = TextSize::ZERO;
for (cell, offset) in cells {
let range = TextRange::new(last_end, offset);
last_end = offset;
let input = source_code.slice(range);
let digit_with = OneIndexed::from_zero_indexed(largest_new.max(largest_old)).digits();
let mut output = String::with_capacity(input.len());
let mut last_end = range.start();
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
writeln!(f, "{:-^1$}", "-", 80)?;
let mut applied = 0;
for edit in self.fix.edits() {
if range.contains_range(edit.range()) {
output.push_str(source_code.slice(TextRange::new(last_end, edit.start())));
output.push_str(edit.content().unwrap_or_default());
last_end = edit.end();
applied += 1;
}
}
for op in group {
for change in diff.iter_inline_changes(op) {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
let line_style = LineStyle::from(change.tag(), self.stylesheet);
// No edits were applied, so there's no need to diff.
if applied == 0 {
continue;
}
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
output.push_str(&source_text[usize::from(last_end)..usize::from(range.end())]);
write!(
f,
"{} {} |{}",
Line {
index: old_index,
width: digit_with
},
Line {
index: new_index,
width: digit_with
},
fmt_styled(line_style.apply_to(sign), self.stylesheet.emphasis),
)?;
let diff = TextDiff::from_lines(input, &output);
for (emphasized, value) in change.iter_strings_lossy() {
let value = show_nonprinting(&value);
if emphasized {
write!(
f,
"{}",
fmt_styled(line_style.apply_to(&value), self.stylesheet.underline)
)?;
} else {
write!(f, "{}", line_style.apply_to(&value))?;
let (largest_old, largest_new) = diff
.ops()
.last()
.map(|op| (op.old_range().start, op.new_range().start))
.unwrap_or_default();
let digit_with = OneIndexed::from_zero_indexed(largest_new.max(largest_old)).digits();
if let Some(cell) = cell {
// Room for 2 digits, 2 x 1 space before each digit, 1 space, and 1 `|`. This
// centers the three colons on the pipe.
writeln!(f, "{:>1$} cell {cell}", ":::", 2 * digit_with.get() + 4)?;
}
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
writeln!(f, "{:-^1$}", "-", 80)?;
}
for op in group {
for change in diff.iter_inline_changes(op) {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
let line_style = LineStyle::from(change.tag(), self.stylesheet);
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
write!(
f,
"{} {} |{}",
Line {
index: old_index,
width: digit_with,
},
Line {
index: new_index,
width: digit_with,
},
fmt_styled(line_style.apply_to(sign), self.stylesheet.emphasis),
)?;
for (emphasized, value) in change.iter_strings_lossy() {
let value = show_nonprinting(&value);
if emphasized {
write!(
f,
"{}",
fmt_styled(
line_style.apply_to(&value),
self.stylesheet.underline
)
)?;
} else {
write!(f, "{}", line_style.apply_to(&value))?;
}
}
if change.missing_newline() {
writeln!(f)?;
}
}
if change.missing_newline() {
writeln!(f)?;
}
}
}
@@ -253,7 +297,7 @@ fn show_nonprinting(s: &str) -> Cow<'_, str> {
#[cfg(test)]
mod tests {
use ruff_diagnostics::Applicability;
use ruff_diagnostics::{Applicability, Fix};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::diagnostic::{
@@ -654,6 +698,107 @@ print()
");
}
/// Test that we remap notebook cell line numbers in the diff as well as the main diagnostic.
#[test]
fn notebook_output_with_diff() {
let (mut env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
env.show_fix_diff(true);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
Safe fix
::: cell 1
1 1 | # cell 1
2 |-import os
error[unused-import][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
Safe fix
::: cell 2
1 1 | # cell 2
2 |-import math
3 2 |
4 3 | print('hello world')
error[unused-variable]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`
Unsafe fix
::: cell 3
1 1 | # cell 3
2 2 | def foo():
3 3 | print()
4 |- x = 1
5 4 |
");
}
#[test]
fn notebook_output_with_diff_spanning_cells() {
let (mut env, mut diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
env.show_fix_diff(true);
// Move all of the edits from the later diagnostics to the first diagnostic to simulate a
// single diagnostic with edits in different cells.
let mut diagnostic = diagnostics.swap_remove(0);
let fix = diagnostic.fix_mut().unwrap();
let mut edits = fix.edits().to_vec();
for diag in diagnostics {
edits.extend_from_slice(diag.fix().unwrap().edits());
}
*fix = Fix::unsafe_edits(edits.remove(0), edits);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[unused-import]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
Unsafe fix
::: cell 1
1 1 | # cell 1
2 |-import os
::: cell 2
1 1 | # cell 2
2 |-import math
3 2 |
4 3 | print('hello world')
::: cell 3
1 1 | # cell 3
2 2 | def foo():
3 3 | print()
4 |- x = 1
5 4 |
");
}
/// Carriage return (`\r`) is a valid line-ending in Python, so we should normalize this to a
/// line feed (`\n`) for rendering. Otherwise we report a single long line for this case.
#[test]

View File

@@ -0,0 +1,205 @@
use std::{
collections::HashSet,
hash::{DefaultHasher, Hash, Hasher},
path::Path,
};
use ruff_source_file::LineColumn;
use serde::{Serialize, Serializer, ser::SerializeSeq};
use crate::diagnostic::{Diagnostic, Severity};
use super::FileResolver;
pub(super) struct GitlabRenderer<'a> {
resolver: &'a dyn FileResolver,
}
impl<'a> GitlabRenderer<'a> {
pub(super) fn new(resolver: &'a dyn FileResolver) -> Self {
Self { resolver }
}
}
impl GitlabRenderer<'_> {
pub(super) fn render(
&self,
f: &mut std::fmt::Formatter,
diagnostics: &[Diagnostic],
) -> std::fmt::Result {
write!(
f,
"{}",
serde_json::to_string_pretty(&SerializedMessages {
diagnostics,
resolver: self.resolver,
#[expect(
clippy::disallowed_methods,
reason = "We don't have access to a `System` here, \
and this is only intended for use by GitLab CI, \
which runs on a real `System`."
)]
project_dir: std::env::var("CI_PROJECT_DIR").ok().as_deref(),
})
.unwrap()
)
}
}
struct SerializedMessages<'a> {
diagnostics: &'a [Diagnostic],
resolver: &'a dyn FileResolver,
project_dir: Option<&'a str>,
}
impl Serialize for SerializedMessages<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?;
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
for diagnostic in self.diagnostics {
let location = diagnostic
.primary_span()
.map(|span| {
let file = span.file();
let positions = if self.resolver.is_notebook(file) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
Default::default()
} else {
let diagnostic_source = file.diagnostic_source(self.resolver);
let source_code = diagnostic_source.as_source_code();
span.range()
.map(|range| Positions {
begin: source_code.line_column(range.start()),
end: source_code.line_column(range.end()),
})
.unwrap_or_default()
};
let path = self.project_dir.as_ref().map_or_else(
|| file.relative_path(self.resolver).display().to_string(),
|project_dir| relativize_path_to(file.path(self.resolver), project_dir),
);
Location { path, positions }
})
.unwrap_or_default();
let mut message_fingerprint = fingerprint(diagnostic, &location.path, 0);
// Make sure that we do not get a fingerprint that is already in use
// by adding in the previously generated one.
while fingerprints.contains(&message_fingerprint) {
message_fingerprint = fingerprint(diagnostic, &location.path, message_fingerprint);
}
fingerprints.insert(message_fingerprint);
let description = diagnostic.body();
let check_name = diagnostic.secondary_code_or_id();
let severity = match diagnostic.severity() {
Severity::Info => "info",
Severity::Warning => "minor",
Severity::Error => "major",
// Another option here is `blocker`
Severity::Fatal => "critical",
};
let value = Message {
check_name,
// GitLab doesn't display the separate `check_name` field in a Code Quality report,
// so prepend it to the description too.
description: format!("{check_name}: {description}"),
severity,
fingerprint: format!("{:x}", message_fingerprint),
location,
};
s.serialize_element(&value)?;
}
s.end()
}
}
#[derive(Serialize)]
struct Message<'a> {
check_name: &'a str,
description: String,
severity: &'static str,
fingerprint: String,
location: Location,
}
/// The place in the source code where the issue was discovered.
///
/// According to the CodeClimate report format [specification] linked from the GitLab [docs], this
/// field is required, so we fall back on a default `path` and position if the diagnostic doesn't
/// have a primary span.
///
/// [specification]: https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
/// [docs]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
#[derive(Default, Serialize)]
struct Location {
path: String,
positions: Positions,
}
#[derive(Default, Serialize)]
struct Positions {
begin: LineColumn,
end: LineColumn,
}
/// Generate a unique fingerprint to identify a violation.
fn fingerprint(diagnostic: &Diagnostic, project_path: &str, salt: u64) -> u64 {
let mut hasher = DefaultHasher::new();
salt.hash(&mut hasher);
diagnostic.name().hash(&mut hasher);
project_path.hash(&mut hasher);
hasher.finish()
}
/// Convert an absolute path to be relative to the specified project root.
fn relativize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root: R) -> String {
format!(
"{}",
pathdiff::diff_paths(&path, project_root)
.expect("Could not diff paths")
.display()
)
}
#[cfg(test)]
mod tests {
use crate::diagnostic::{
DiagnosticFormat,
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
};
const FINGERPRINT_FILTERS: [(&str, &str); 1] = [(
r#""fingerprint": "[a-z0-9]+","#,
r#""fingerprint": "<redacted>","#,
)];
#[test]
fn output() {
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Gitlab);
insta::with_settings!({filters => FINGERPRINT_FILTERS}, {
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
});
}
#[test]
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Gitlab);
insta::with_settings!({filters => FINGERPRINT_FILTERS}, {
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
});
}
}

View File

@@ -1,63 +1,63 @@
---
source: crates/ruff_linter/src/message/gitlab.rs
expression: redact_fingerprint(&content)
source: crates/ruff_db/src/diagnostic/render/gitlab.rs
expression: env.render_diagnostics(&diagnostics)
---
[
{
"check_name": "F401",
"description": "F401: `os` imported but unused",
"severity": "major",
"fingerprint": "<redacted>",
"location": {
"path": "fib.py",
"positions": {
"begin": {
"column": 8,
"line": 1
"line": 1,
"column": 8
},
"end": {
"column": 10,
"line": 1
"line": 1,
"column": 10
}
}
},
"severity": "major"
}
},
{
"check_name": "F841",
"description": "F841: Local variable `x` is assigned to but never used",
"severity": "major",
"fingerprint": "<redacted>",
"location": {
"path": "fib.py",
"positions": {
"begin": {
"column": 5,
"line": 6
"line": 6,
"column": 5
},
"end": {
"column": 6,
"line": 6
"line": 6,
"column": 6
}
}
},
"severity": "major"
}
},
{
"check_name": "F821",
"description": "F821: Undefined name `a`",
"severity": "major",
"fingerprint": "<redacted>",
"location": {
"path": "undef.py",
"positions": {
"begin": {
"column": 4,
"line": 1
"line": 1,
"column": 4
},
"end": {
"column": 5,
"line": 1
"line": 1,
"column": 5
}
}
},
"severity": "major"
}
}
]

View File

@@ -1,44 +1,44 @@
---
source: crates/ruff_linter/src/message/gitlab.rs
expression: redact_fingerprint(&content)
source: crates/ruff_db/src/diagnostic/render/gitlab.rs
expression: env.render_diagnostics(&diagnostics)
---
[
{
"check_name": "invalid-syntax",
"description": "invalid-syntax: Expected one or more symbol names after import",
"severity": "major",
"fingerprint": "<redacted>",
"location": {
"path": "syntax_errors.py",
"positions": {
"begin": {
"column": 15,
"line": 1
"line": 1,
"column": 15
},
"end": {
"column": 1,
"line": 2
"line": 2,
"column": 1
}
}
},
"severity": "major"
}
},
{
"check_name": "invalid-syntax",
"description": "invalid-syntax: Expected ')', found newline",
"severity": "major",
"fingerprint": "<redacted>",
"location": {
"path": "syntax_errors.py",
"positions": {
"begin": {
"column": 12,
"line": 3
"line": 3,
"column": 12
},
"end": {
"column": 1,
"line": 4
"line": 4,
"column": 1
}
}
},
"severity": "major"
}
}
]

View File

@@ -9,17 +9,7 @@ use crate::system::file_time_now;
/// * The last modification time of the file.
/// * The hash of the file's content.
/// * The revision as it comes from an external system, for example the LSP.
#[derive(
Copy,
Clone,
Debug,
Eq,
PartialEq,
Default,
get_size2::GetSize,
serde::Serialize,
serde::Deserialize,
)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)]
pub struct FileRevision(u128);
impl FileRevision {

View File

@@ -14,7 +14,7 @@ use crate::diagnostic::{Span, UnifiedFile};
use crate::file_revision::FileRevision;
use crate::files::file_root::FileRoots;
use crate::files::private::FileStatus;
use crate::system::{FileType, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf};
use crate::system::{SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf};
use crate::vendored::{VendoredPath, VendoredPathBuf};
use crate::{Db, FxDashMap, vendored};
@@ -139,7 +139,6 @@ impl Files {
};
tracing::trace!("Adding vendored file `{}`", path);
let file = File::builder(FilePath::Vendored(path.to_path_buf()))
.permissions(Some(0o444))
.revision(metadata.revision())
@@ -201,15 +200,7 @@ impl Files {
let mut roots = self.inner.roots.write().unwrap();
let absolute = SystemPath::absolute(path, db.system().current_directory());
let (Ok(root) | Err(root)) = roots.try_add(db, absolute, |absolute| {
FileRoot::builder(absolute, kind, FileRevision::now())
.durability(Durability::HIGH)
.revision_durability(kind.durability())
.new(db)
});
root
roots.try_add(db, absolute, kind)
}
/// Updates the revision of the root for `path`.
@@ -268,51 +259,6 @@ impl Files {
root.set_revision(db).to(FileRevision::now());
}
}
/// Seed the files with an existing [`File`] instance.
pub fn seed(&self, file: File, db: &dyn Db) {
let seeded = match file.path(db) {
FilePath::System(path) => self
.inner
.system_by_path
.insert(path.clone(), file)
.is_none(),
FilePath::SystemVirtual(path) => self
.inner
.system_virtual_by_path
.insert(path.clone(), VirtualFile(file))
.is_none(),
FilePath::Vendored(path) => self
.inner
.vendored_by_path
.insert(path.clone(), file)
.is_none(),
};
// Recreating a `File` input means the persisted queries depending on that file
// will be invalidated.
assert!(
seeded,
"unexpected `File` input recreated for path `{}`",
file.path(db)
);
}
/// Seed the files with an existing [`FileRoot`] instance.
pub fn seed_root(&self, root: FileRoot, db: &dyn Db) {
let mut roots = self.inner.roots.write().unwrap();
let seeded = roots
.try_add(db, root.path(db).to_path_buf(), |_| root)
.is_ok();
// Recreating a `FileRoot` input means the persisted queries depending on that file
// root will be invalidated.
assert!(
seeded,
"unexpected `FileRoot` input recreated for path `{}`",
root.path(db)
);
}
}
impl fmt::Debug for Files {
@@ -344,7 +290,7 @@ impl std::panic::RefUnwindSafe for Files {}
/// # Ordering
/// Ordering is based on the file's salsa-assigned id and not on its values.
/// The id may change between runs.
#[salsa::input(persist, heap_size=ruff_memory_usage::heap_size)]
#[salsa::input(heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct File {
/// The path of the file (immutable).
@@ -468,15 +414,6 @@ impl File {
}
}
/// Loads all existing [`File`]s in the database.
pub fn load_all(db: &dyn Db) -> Vec<File> {
// TODO: Prune deleted paths.
File::ingredient(db)
.entries(db.zalsa())
.map(|entry| entry.as_struct())
.collect()
}
/// Private method providing the implementation for [`Self::sync_path`] and [`Self::sync`] for
/// system paths.
fn sync_system_path(db: &mut dyn Db, path: &SystemPath, file: Option<File>) {
@@ -585,17 +522,7 @@ impl VirtualFile {
// The types in here need to be public because they're salsa ingredients but we
// don't want them to be publicly accessible. That's why we put them into a private module.
mod private {
#[derive(
Copy,
Clone,
Debug,
Eq,
PartialEq,
Default,
get_size2::GetSize,
serde::Serialize,
serde::Deserialize,
)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)]
pub enum FileStatus {
/// The file exists.
#[default]
@@ -609,16 +536,6 @@ mod private {
}
}
impl From<FileType> for FileStatus {
fn from(value: FileType) -> Self {
match value {
FileType::File => FileStatus::Exists,
FileType::Symlink => FileStatus::Exists,
FileType::Directory => FileStatus::IsADirectory,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum FileError {
IsADirectory,

View File

@@ -16,7 +16,7 @@ use crate::system::{SystemPath, SystemPathBuf};
/// The main usage of file roots is to determine a file's durability. But it can also be used
/// to make a salsa query dependent on whether a file in a root has changed without writing any
/// manual invalidation logic.
#[salsa::input(persist, debug, heap_size=ruff_memory_usage::heap_size)]
#[salsa::input(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct FileRoot {
/// The path of a root is guaranteed to never change.
#[returns(deref)]
@@ -35,20 +35,9 @@ impl FileRoot {
pub fn durability(self, db: &dyn Db) -> salsa::Durability {
self.kind_at_time_of_creation(db).durability()
}
/// Loads all existing [`FileRoot`]s in the database.
pub fn load_all(db: &dyn Db) -> Vec<FileRoot> {
// TODO: Prune deleted paths.
FileRoot::ingredient(db)
.entries(db.zalsa())
.map(|entry| entry.as_struct())
.collect()
}
}
#[derive(
Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize, serde::Serialize, serde::Deserialize,
)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
pub enum FileRootKind {
/// The root of a project.
Project,
@@ -58,7 +47,7 @@ pub enum FileRootKind {
}
impl FileRootKind {
pub const fn durability(self) -> Durability {
const fn durability(self) -> Durability {
match self {
FileRootKind::Project => Durability::LOW,
FileRootKind::LibrarySearchPath => Durability::HIGH,
@@ -73,34 +62,34 @@ pub(super) struct FileRoots {
}
impl FileRoots {
/// Tries to add a new root for `path`.
/// Tries to add a new root for `path` and returns the root.
///
/// The root isn't added nor is the file root's kind updated if a root for `path` already exists.
///
/// Returns `Ok(root)` if the `FileRoot` was successfully added, and returns `Err(root)` with
/// the previous root if one already existed at that path.
pub(super) fn try_add(
&mut self,
db: &dyn Db,
path: SystemPathBuf,
create_root: impl FnOnce(SystemPathBuf) -> FileRoot,
) -> Result<FileRoot, FileRoot> {
kind: FileRootKind,
) -> FileRoot {
// SAFETY: Guaranteed to succeed because `path` is a UTF-8 that only contains Unicode characters.
let normalized_path = path.as_std_path().to_slash().unwrap();
if let Ok(existing) = self.by_path.at(&normalized_path) {
// Only if it is an exact match
if existing.value.path(db) == &*path {
return Err(*existing.value);
return *existing.value;
}
}
// Normalize the path to use `/` separators and escape the '{' and '}' characters,
// which `matchit` uses for routing parameters.
// normalize the path to use `/` separators and escape the '{' and '}' characters,
// which matchit uses for routing parameters
let mut route = normalized_path.replace('{', "{{").replace('}', "}}");
// Insert a new source root
let root = create_root(path);
let root = FileRoot::builder(path, kind, FileRevision::now())
.durability(Durability::HIGH)
.revision_durability(kind.durability())
.new(db);
// Insert a path that matches the root itself
self.by_path.insert(route.clone(), root).unwrap();
@@ -111,7 +100,7 @@ impl FileRoots {
self.by_path.insert(route, root).unwrap();
self.roots.push(root);
Ok(root)
root
}
/// Returns the closest root for `path` or `None` if no root contains `path`.

View File

@@ -11,9 +11,7 @@ use std::fmt::{Display, Formatter};
/// * a file stored on the [host system](crate::system::System).
/// * a virtual file stored on the [host system](crate::system::System).
/// * a vendored file stored in the [vendored file system](crate::vendored::VendoredFileSystem).
#[derive(
Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize, serde::Serialize, serde::Deserialize,
)]
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
pub enum FilePath {
/// Path to a file on the [host system](crate::system::System).
System(SystemPathBuf),

View File

@@ -148,16 +148,7 @@ impl From<Notebook> for SourceTextKind {
}
}
#[derive(
Debug,
thiserror::Error,
PartialEq,
Eq,
Clone,
get_size2::GetSize,
serde::Serialize,
serde::Deserialize,
)]
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone, get_size2::GetSize)]
pub enum SourceTextError {
#[error("Failed to read notebook: {0}`")]
FailedToReadNotebook(String),

View File

@@ -66,9 +66,6 @@ pub trait System: Debug + Sync + Send {
/// See [dunce::canonicalize] for more information.
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf>;
/// Reads the content of the file at `path` into a bytes buffer.
fn read_to_end(&self, path: &SystemPath) -> Result<Vec<u8>>;
/// Reads the content of the file at `path` into a [`String`].
fn read_to_string(&self, path: &SystemPath) -> Result<String>;
@@ -245,7 +242,7 @@ pub trait WritableSystem: System {
fn create_new_file(&self, path: &SystemPath) -> Result<()>;
/// Writes the given content to the file at the given path.
fn write_file(&self, path: &SystemPath, content: &[u8]) -> Result<()>;
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()>;
/// Creates a directory at `path` as well as any intermediate directories.
fn create_directory_all(&self, path: &SystemPath) -> Result<()>;
@@ -281,7 +278,7 @@ pub trait WritableSystem: System {
// ensures that only one thread/process ever attempts to write to it to avoid corrupting
// the cache.
self.create_new_file(&cache_path)?;
self.write_file(&cache_path, contents.as_bytes())?;
self.write_file(&cache_path, &contents)?;
Ok(Some(cache_path))
}

View File

@@ -114,8 +114,8 @@ impl MemoryFileSystem {
matches!(by_path.get(&normalized), Some(Entry::Directory(_)))
}
pub fn read_to_end(&self, path: impl AsRef<SystemPath>) -> Result<Vec<u8>> {
fn read_to_end(fs: &MemoryFileSystem, path: &SystemPath) -> Result<Vec<u8>> {
pub fn read_to_string(&self, path: impl AsRef<SystemPath>) -> Result<String> {
fn read_to_string(fs: &MemoryFileSystem, path: &SystemPath) -> Result<String> {
let by_path = fs.inner.by_path.read().unwrap();
let normalized = fs.normalize_path(path);
@@ -127,32 +127,19 @@ impl MemoryFileSystem {
}
}
read_to_end(self, path.as_ref())
}
pub fn read_to_string(&self, path: impl AsRef<SystemPath>) -> Result<String> {
self.read_to_end(path)
.and_then(|bytes| String::from_utf8(bytes).map_err(io::Error::other))
}
pub(crate) fn read_virtual_path_to_end(
&self,
path: impl AsRef<SystemVirtualPath>,
) -> Result<Vec<u8>> {
let virtual_files = self.inner.virtual_files.read().unwrap();
let file = virtual_files
.get(&path.as_ref().to_path_buf())
.ok_or_else(not_found)?;
Ok(file.content.clone())
read_to_string(self, path.as_ref())
}
pub(crate) fn read_virtual_path_to_string(
&self,
path: impl AsRef<SystemVirtualPath>,
) -> Result<String> {
self.read_virtual_path_to_end(path)
.and_then(|bytes| String::from_utf8(bytes).map_err(io::Error::other))
let virtual_files = self.inner.virtual_files.read().unwrap();
let file = virtual_files
.get(&path.as_ref().to_path_buf())
.ok_or_else(not_found)?;
Ok(file.content.clone())
}
pub fn exists(&self, path: &SystemPath) -> bool {
@@ -174,7 +161,7 @@ impl MemoryFileSystem {
match by_path.entry(normalized) {
btree_map::Entry::Vacant(entry) => {
entry.insert(Entry::File(File {
content: Vec::new(),
content: String::new(),
last_modified: file_time_now(),
}));
@@ -190,17 +177,13 @@ impl MemoryFileSystem {
/// Stores a new file in the file system.
///
/// The operation overrides the content for an existing file with the same normalized `path`.
pub fn write_file(
&self,
path: impl AsRef<SystemPath>,
content: impl Into<Vec<u8>>,
) -> Result<()> {
pub fn write_file(&self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
let mut by_path = self.inner.by_path.write().unwrap();
let normalized = self.normalize_path(path.as_ref());
let file = get_or_create_file(&mut by_path, &normalized)?;
file.content = content.into();
file.content = content.to_string();
file.last_modified = file_time_now();
Ok(())
@@ -231,7 +214,7 @@ impl MemoryFileSystem {
pub fn write_file_all(
&self,
path: impl AsRef<SystemPath>,
content: impl Into<Vec<u8>>,
content: impl ToString,
) -> Result<()> {
let path = path.as_ref();
@@ -245,23 +228,19 @@ impl MemoryFileSystem {
/// Stores a new virtual file in the file system.
///
/// The operation overrides the content for an existing virtual file with the same `path`.
pub fn write_virtual_file(
&self,
path: impl AsRef<SystemVirtualPath>,
content: impl Into<Vec<u8>>,
) {
pub fn write_virtual_file(&self, path: impl AsRef<SystemVirtualPath>, content: impl ToString) {
let path = path.as_ref();
let mut virtual_files = self.inner.virtual_files.write().unwrap();
match virtual_files.entry(path.to_path_buf()) {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(File {
content: content.into(),
content: content.to_string(),
last_modified: file_time_now(),
});
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
entry.get_mut().content = content.into();
entry.get_mut().content = content.to_string();
}
}
}
@@ -489,7 +468,7 @@ impl Entry {
#[derive(Debug)]
struct File {
content: Vec<u8>,
content: String,
last_modified: FileTime,
}
@@ -554,7 +533,7 @@ fn get_or_create_file<'a>(
let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
Entry::File(File {
content: Vec::new(),
content: String::new(),
last_modified: file_time_now(),
})
});

View File

@@ -93,10 +93,6 @@ impl System for OsSystem {
})
}
fn read_to_end(&self, path: &SystemPath) -> Result<Vec<u8>> {
std::fs::read(path.as_std_path())
}
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
std::fs::read_to_string(path.as_std_path())
}
@@ -361,7 +357,7 @@ impl WritableSystem for OsSystem {
std::fs::File::create_new(path).map(drop)
}
fn write_file(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
std::fs::write(path.as_std_path(), content)
}

View File

@@ -762,17 +762,7 @@ impl SystemVirtualPath {
}
/// An owned, virtual path on [`System`](`super::System`) (akin to [`String`]).
#[derive(
Eq,
PartialEq,
Clone,
Hash,
PartialOrd,
Ord,
get_size2::GetSize,
serde::Serialize,
serde::Deserialize,
)]
#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord, get_size2::GetSize)]
pub struct SystemVirtualPathBuf(String);
impl SystemVirtualPathBuf {

View File

@@ -75,10 +75,6 @@ impl System for TestSystem {
self.system().canonicalize_path(path)
}
fn read_to_end(&self, path: &SystemPath) -> Result<Vec<u8>> {
self.system().read_to_end(path)
}
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
self.system().read_to_string(path)
}
@@ -169,7 +165,7 @@ impl WritableSystem for TestSystem {
self.system().create_new_file(path)
}
fn write_file(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
self.system().write_file(path, content)
}
@@ -189,9 +185,7 @@ pub trait DbWithWritableSystem: Db + Sized {
/// Writes the content of the given file and notifies the Db about the change.
fn write_file(&mut self, path: impl AsRef<SystemPath>, content: impl AsRef<str>) -> Result<()> {
let path = path.as_ref();
let content = content.as_ref();
match self.writable_system().write_file(path, content.as_bytes()) {
match self.writable_system().write_file(path, content.as_ref()) {
Ok(()) => {
File::sync_path(self, path);
Ok(())
@@ -204,8 +198,7 @@ pub trait DbWithWritableSystem: Db + Sized {
File::sync_path(self, ancestor);
}
self.writable_system()
.write_file(path, content.as_bytes())?;
self.writable_system().write_file(path, content.as_ref())?;
File::sync_path(self, path);
Ok(())
@@ -250,14 +243,8 @@ pub trait DbWithTestSystem: Db + Sized {
///
/// ## Panics
/// If the db isn't using the [`InMemorySystem`].
fn write_virtual_file(
&mut self,
path: impl AsRef<SystemVirtualPath>,
content: impl Into<Vec<u8>>,
) {
fn write_virtual_file(&mut self, path: impl AsRef<SystemVirtualPath>, content: impl ToString) {
let path = path.as_ref();
let content = content.into();
self.test_system()
.memory_file_system()
.write_virtual_file(path, content);
@@ -335,10 +322,6 @@ impl System for InMemorySystem {
self.memory_fs.canonicalize(path)
}
fn read_to_end(&self, path: &SystemPath) -> Result<Vec<u8>> {
self.memory_fs.read_to_end(path)
}
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
self.memory_fs.read_to_string(path)
}
@@ -429,7 +412,7 @@ impl WritableSystem for InMemorySystem {
self.memory_fs.create_new_file(path)
}
fn write_file(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
self.memory_fs.write_file(path, content)
}

View File

@@ -88,7 +88,7 @@ impl ToOwned for VendoredPath {
}
#[repr(transparent)]
#[derive(Debug, Eq, PartialEq, Clone, Hash, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub struct VendoredPathBuf(Utf8PathBuf);
impl get_size2::GetSize for VendoredPathBuf {

View File

@@ -13,6 +13,7 @@ license = { workspace = true }
[dependencies]
ty = { workspace = true }
ty_project = { workspace = true, features = ["schemars"] }
ty_python_semantic = { workspace = true }
ty_static = { workspace = true }
ruff = { workspace = true }
ruff_formatter = { workspace = true }

View File

@@ -52,7 +52,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
}
fn generate_markdown() -> String {
let registry = &*ty_project::DEFAULT_LINT_REGISTRY;
let registry = ty_python_semantic::default_lint_registry();
let mut output = String::new();

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.12.10"
version = "0.12.11"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -51,7 +51,6 @@ path-absolutize = { workspace = true, features = [
"once_cell_cache",
"use_unix_paths_on_wasm",
] }
pathdiff = { workspace = true }
pep440_rs = { workspace = true }
pyproject-toml = { workspace = true }
regex = { workspace = true }

View File

@@ -34,7 +34,7 @@ task_group()
setup()
from airflow.decorators import teardown
from airflow.io.path import ObjectStoragePath
from airflow.io.storage import attach
from airflow.io.store import attach
from airflow.models import DAG as DAGFromModel
from airflow.models import (
Connection,
@@ -74,3 +74,36 @@ DatasetOrTimeSchedule()
# airflow.utils.dag_parsing_context
get_parsing_context()
from airflow.decorators.base import (
DecoratedMappedOperator,
DecoratedOperator,
TaskDecorator,
get_unique_task_id,
task_decorator_factory,
)
# airflow.decorators.base
DecoratedMappedOperator()
DecoratedOperator()
TaskDecorator()
get_unique_task_id()
task_decorator_factory()
from airflow.models import Param
# airflow.models
Param()
from airflow.sensors.base import (
BaseSensorOperator,
PokeReturnValue,
poke_mode_only,
)
# airflow.sensors.base
BaseSensorOperator()
PokeReturnValue()
poke_mode_only()

View File

@@ -9,7 +9,6 @@ from airflow.operators.empty import EmptyOperator
from airflow.operators.latest_only import LatestOnlyOperator
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
from airflow.operators.weekday import BranchDayOfWeekOperator
from airflow.sensors.date_time import DateTimeSensor
FSHook()
PackageIndexHook()
@@ -22,7 +21,6 @@ EmptyOperator()
LatestOnlyOperator()
BranchDayOfWeekOperator()
DateTimeSensor()
from airflow.operators.python import (
BranchPythonOperator,
@@ -30,16 +28,23 @@ from airflow.operators.python import (
PythonVirtualenvOperator,
ShortCircuitOperator,
)
from airflow.sensors.bash import BashSensor
from airflow.sensors.date_time import DateTimeSensor
BranchPythonOperator()
PythonOperator()
PythonVirtualenvOperator()
ShortCircuitOperator()
BashSensor()
DateTimeSensor()
from airflow.sensors.date_time import DateTimeSensorAsync
from airflow.sensors.external_task import (
ExternalTaskMarker,
ExternalTaskSensor,
)
from airflow.sensors.time_sensor import (
TimeSensor,
TimeSensorAsync,
)
from airflow.sensors.filesystem import FileSensor
from airflow.sensors.python import PythonSensor
BranchPythonOperator()
PythonOperator()
@@ -49,6 +54,13 @@ DateTimeSensorAsync()
ExternalTaskMarker()
ExternalTaskSensor()
FileSensor()
PythonSensor()
from airflow.sensors.time_sensor import (
TimeSensor,
TimeSensorAsync,
)
TimeSensor()
TimeSensorAsync()

View File

@@ -0,0 +1,75 @@
from typing import Optional
import httpx
def foo():
client = httpx.Client()
client.close() # Ok
client.delete() # Ok
client.get() # Ok
client.head() # Ok
client.options() # Ok
client.patch() # Ok
client.post() # Ok
client.put() # Ok
client.request() # Ok
client.send() # Ok
client.stream() # Ok
client.anything() # Ok
client.build_request() # Ok
client.is_closed # Ok
async def foo():
client = httpx.Client()
client.close() # ASYNC212
client.delete() # ASYNC212
client.get() # ASYNC212
client.head() # ASYNC212
client.options() # ASYNC212
client.patch() # ASYNC212
client.post() # ASYNC212
client.put() # ASYNC212
client.request() # ASYNC212
client.send() # ASYNC212
client.stream() # ASYNC212
client.anything() # Ok
client.build_request() # Ok
client.is_closed # Ok
async def foo(client: httpx.Client):
client.request() # ASYNC212
client.anything() # Ok
async def foo(client: httpx.Client | None):
client.request() # ASYNC212
client.anything() # Ok
async def foo(client: Optional[httpx.Client]):
client.request() # ASYNC212
client.anything() # Ok
async def foo():
client: httpx.Client = ...
client.request() # ASYNC212
client.anything() # Ok
global_client = httpx.Client()
async def foo():
global_client.request() # ASYNC212
global_client.anything() # Ok
async def foo():
async with httpx.AsyncClient() as client:
await client.get() # Ok

View File

@@ -17,3 +17,50 @@ info(f"{__name__}")
# Don't trigger for t-strings
info(t"{name}")
info(t"{__name__}")
count = 5
total = 9
directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
logging.info(f"{count} out of {total} files in {directory_path} checked")
x = 99
fmt = "08d"
logger.info(f"{x:{'08d'}}")
logger.info(f"{x:>10} {x:{fmt}}")
logging.info(f"")
logging.info(f"This message doesn't have any variables.")
obj = {"key": "value"}
logging.info(f"Object: {obj!r}")
items_count = 3
logging.warning(f"Items: {items_count:d}")
data = {"status": "active"}
logging.info(f"Processing {len(data)} items")
logging.info(f"Status: {data.get('status', 'unknown').upper()}")
result = 123
logging.info(f"Calculated result: {result + 100}")
temperature = 123
logging.info(f"Temperature: {temperature:.1f}°C")
class FilePath:
def __init__(self, name: str):
self.name = name
logging.info(f"No changes made to {file_path.name}.")
user = "tron"
balance = 123.45
logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}")
import logging
x = 1
logging.error(f"{x} -> %s", x)

View File

@@ -0,0 +1,10 @@
"""Test f-string argument order."""
import logging
logger = logging.getLogger(__name__)
X = 1
Y = 2
logger.error(f"{X} -> %s", Y)
logger.error(f"{Y} -> %s", X)

View File

@@ -0,0 +1,16 @@
"""
Test: allowed-unused-imports-top-level-module
"""
# No errors
def f():
import hvplot
def f():
import hvplot.pandas
def f():
import hvplot.pandas.plots
def f():
from hvplot.pandas import scatter_matrix
def f():
from hvplot.pandas.plots import scatter_matrix

View File

@@ -151,3 +151,39 @@ def f():
pass
except Exception as _:
pass
# OK, `__class__` in this case is not the special `__class__` cell, so we don't
# emit a diagnostic. (It has its own special semantics -- see
# https://github.com/astral-sh/ruff/pull/20048#discussion_r2298338048 -- but
# those aren't relevant here.)
class A:
__class__ = 1
# The following three cases are flagged because they declare local `__class__`
# variables that don't refer to the special `__class__` cell.
class A:
def set_class(self, cls):
__class__ = cls # F841
class A:
class B:
def set_class(self, cls):
__class__ = cls # F841
class A:
def foo():
class B:
print(__class__)
def set_class(self, cls):
__class__ = cls # F841
# OK, the `__class__` cell is nonlocal and declared as such.
class NonlocalDunderClass:
def foo():
nonlocal __class__
__class__ = 1

View File

@@ -44,3 +44,8 @@ def f():
def g():
nonlocal x
x = 2
# OK
class A:
def method(self):
nonlocal __class__

View File

@@ -124,3 +124,19 @@ def fun_with_python_syntax():
...
return Foo
@dataclass
class C:
def __post_init__(self, x: tuple[int, ...] = (
1,
2,
)) -> None:
self.x = x
@dataclass
class D:
def __post_init__(self, x: int = """
""") -> None:
self.x = x

View File

@@ -660,6 +660,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::BlockingHttpCallInAsyncFunction) {
flake8_async::rules::blocking_http_call(checker, call);
}
if checker.is_rule_enabled(Rule::BlockingHttpCallHttpxInAsyncFunction) {
flake8_async::rules::blocking_http_call_httpx(checker, call);
}
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
flake8_async::rules::blocking_open_call(checker, call);
}
@@ -1046,7 +1049,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::PyPath,
Rule::Glob,
Rule::OsListdir,
Rule::OsSymlink,
]) {
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
}

View File

@@ -703,7 +703,10 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Lambda(_) => return false,
ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) => return *is_async,
ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type => {}
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
}
}
false
@@ -714,7 +717,10 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type => {}
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
}
}
false
@@ -725,7 +731,7 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Generator { .. } => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Module | ScopeKind::Type => {}
ScopeKind::Module | ScopeKind::Type | ScopeKind::DunderClassCell => {}
}
}
false
@@ -1092,6 +1098,24 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}
// Here we add the implicit scope surrounding a method which allows code in the
// method to access `__class__` at runtime. See the `ScopeKind::DunderClassCell`
// docs for more information.
let added_dunder_class_scope = if self.semantic.current_scope().kind.is_class() {
self.semantic.push_scope(ScopeKind::DunderClassCell);
let binding_id = self.semantic.push_binding(
TextRange::default(),
BindingKind::DunderClassCell,
BindingFlags::empty(),
);
self.semantic
.current_scope_mut()
.add("__class__", binding_id);
true
} else {
false
};
self.semantic.push_scope(ScopeKind::Type);
if let Some(type_params) = type_params {
@@ -1155,6 +1179,9 @@ impl<'a> Visitor<'a> for Checker<'a> {
self.semantic.pop_scope(); // Function scope
self.semantic.pop_definition();
self.semantic.pop_scope(); // Type parameter scope
if added_dunder_class_scope {
self.semantic.pop_scope(); // `__class__` cell closure scope
}
self.add_binding(
name,
stmt.identifier(),

View File

@@ -336,6 +336,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
(Flake8Async, "212") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction),
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),

View File

@@ -58,13 +58,3 @@ pub fn relativize_path<P: AsRef<Path>>(path: P) -> String {
}
format!("{}", path.display())
}
/// Convert an absolute path to be relative to the specified project root.
pub fn relativize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root: R) -> String {
format!(
"{}",
pathdiff::diff_paths(&path, project_root)
.expect("Could not diff paths")
.display()
)
}

View File

@@ -1,174 +0,0 @@
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::Write;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
use ruff_db::diagnostic::Diagnostic;
use crate::fs::{relativize_path, relativize_path_to};
use crate::message::{Emitter, EmitterContext};
/// Generate JSON with violations in GitLab CI format
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
pub struct GitlabEmitter {
project_dir: Option<String>,
}
impl Default for GitlabEmitter {
fn default() -> Self {
Self {
project_dir: std::env::var("CI_PROJECT_DIR").ok(),
}
}
}
impl Emitter for GitlabEmitter {
fn emit(
&mut self,
writer: &mut dyn Write,
diagnostics: &[Diagnostic],
context: &EmitterContext,
) -> anyhow::Result<()> {
serde_json::to_writer_pretty(
writer,
&SerializedMessages {
diagnostics,
context,
project_dir: self.project_dir.as_deref(),
},
)?;
Ok(())
}
}
struct SerializedMessages<'a> {
diagnostics: &'a [Diagnostic],
context: &'a EmitterContext<'a>,
project_dir: Option<&'a str>,
}
impl Serialize for SerializedMessages<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?;
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
for diagnostic in self.diagnostics {
let filename = diagnostic.expect_ruff_filename();
let (start_location, end_location) = if self.context.is_notebook(&filename) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
Default::default()
} else {
(
diagnostic.expect_ruff_start_location(),
diagnostic.expect_ruff_end_location(),
)
};
let path = self.project_dir.as_ref().map_or_else(
|| relativize_path(&filename),
|project_dir| relativize_path_to(&filename, project_dir),
);
let mut message_fingerprint = fingerprint(diagnostic, &path, 0);
// Make sure that we do not get a fingerprint that is already in use
// by adding in the previously generated one.
while fingerprints.contains(&message_fingerprint) {
message_fingerprint = fingerprint(diagnostic, &path, message_fingerprint);
}
fingerprints.insert(message_fingerprint);
let description = diagnostic.body();
let check_name = diagnostic.secondary_code_or_id();
let value = json!({
"check_name": check_name,
// GitLab doesn't display the separate `check_name` field in a Code Quality report,
// so prepend it to the description too.
"description": format!("{check_name}: {description}"),
"severity": "major",
"fingerprint": format!("{:x}", message_fingerprint),
"location": {
"path": path,
"positions": {
"begin": start_location,
"end": end_location,
},
},
});
s.serialize_element(&value)?;
}
s.end()
}
}
/// Generate a unique fingerprint to identify a violation.
fn fingerprint(message: &Diagnostic, project_path: &str, salt: u64) -> u64 {
let mut hasher = DefaultHasher::new();
salt.hash(&mut hasher);
message.name().hash(&mut hasher);
project_path.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::GitlabEmitter;
use crate::message::tests::{
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
};
#[test]
fn output() {
let mut emitter = GitlabEmitter::default();
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(redact_fingerprint(&content));
}
#[test]
fn syntax_errors() {
let mut emitter = GitlabEmitter::default();
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
assert_snapshot!(redact_fingerprint(&content));
}
// Redact the fingerprint because the default hasher isn't stable across platforms.
fn redact_fingerprint(content: &str) -> String {
static FINGERPRINT_HAY_KEY: &str = r#""fingerprint": ""#;
let mut output = String::with_capacity(content.len());
let mut last = 0;
for (start, _) in content.match_indices(FINGERPRINT_HAY_KEY) {
let fingerprint_hash_start = start + FINGERPRINT_HAY_KEY.len();
output.push_str(&content[last..fingerprint_hash_start]);
output.push_str("<redacted>");
last = fingerprint_hash_start
+ content[fingerprint_hash_start..]
.find('"')
.expect("Expected terminating quote");
}
output.push_str(&content[last..]);
output
}
}

View File

@@ -10,7 +10,6 @@ use ruff_db::diagnostic::{
use ruff_db::files::File;
pub use github::GithubEmitter;
pub use gitlab::GitlabEmitter;
pub use grouped::GroupedEmitter;
use ruff_notebook::NotebookIndex;
use ruff_source_file::SourceFile;
@@ -22,7 +21,6 @@ use crate::Fix;
use crate::registry::Rule;
mod github;
mod gitlab;
mod grouped;
mod sarif;
mod text;

View File

@@ -40,6 +40,11 @@ pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSet
settings.preview.is_enabled()
}
/// <https://github.com/astral-sh/ruff/pull/19303>
pub(crate) const fn is_fix_f_string_logging_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/16719
pub(crate) const fn is_fix_manual_dict_comprehension_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()

View File

@@ -354,7 +354,10 @@ impl Renamer {
))
}
// Avoid renaming builtins and other "special" bindings.
BindingKind::FutureImport | BindingKind::Builtin | BindingKind::Export(_) => None,
BindingKind::FutureImport
| BindingKind::Builtin
| BindingKind::Export(_)
| BindingKind::DunderClassCell => None,
// By default, replace the binding's name with the target name.
BindingKind::Annotation
| BindingKind::Argument

View File

@@ -215,6 +215,12 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
version: "0.0.1",
}
}
["airflow", "sensors", "bash", "BashSensor"] => ProviderReplacement::AutoImport {
module: "airflow.providers.standard.sensor.bash",
name: "BashSensor",
provider: "standard",
version: "0.0.1",
},
[
"airflow",
"sensors",
@@ -243,6 +249,12 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
provider: "standard",
version: "0.0.2",
},
["airflow", "sensors", "python", "PythonSensor"] => ProviderReplacement::AutoImport {
module: "airflow.providers.standard.sensors.python",
name: "PythonSensor",
provider: "standard",
version: "0.0.1",
},
[
"airflow",
"sensors",

View File

@@ -227,13 +227,26 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.sdk",
name: (*rest).to_string(),
},
[
"airflow",
"decorators",
"base",
rest @ ("DecoratedMappedOperator"
| "DecoratedOperator"
| "TaskDecorator"
| "get_unique_task_id"
| "task_decorator_factory"),
] => Replacement::SourceModuleMoved {
module: "airflow.sdk.bases.decorator",
name: (*rest).to_string(),
},
// airflow.io
["airflow", "io", "path", "ObjectStoragePath"] => Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: "ObjectStoragePath".to_string(),
},
["airflow", "io", "storage", "attach"] => Replacement::SourceModuleMoved {
["airflow", "io", "store", "attach"] => Replacement::SourceModuleMoved {
module: "airflow.sdk.io",
name: "attach".to_string(),
},
@@ -245,6 +258,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
name: (*rest).to_string(),
}
}
["airflow", "models", "Param"] => Replacement::AutoImport {
module: "airflow.sdk.definitions.param",
name: "Param",
},
// airflow.models.baseoperator
[
@@ -260,16 +277,30 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.sdk",
name: "BaseOperatorLink",
},
// airflow.model..DAG
["airflow", "models", .., "DAG"] => Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: "DAG".to_string(),
},
// airflow.sensors.base
[
"airflow",
"sensors",
"base",
rest @ ("BaseSensorOperator" | "PokeReturnValue" | "poke_mode_only"),
] => Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: (*rest).to_string(),
},
// airflow.timetables
["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => Replacement::AutoImport {
module: "airflow.timetables.assets",
name: "AssetOrTimeSchedule",
},
// airflow.utils
[
"airflow",

View File

@@ -312,7 +312,7 @@ help: Use `teardown` from `airflow.sdk` instead.
34 34 | setup()
35 |-from airflow.decorators import teardown
36 35 | from airflow.io.path import ObjectStoragePath
37 36 | from airflow.io.storage import attach
37 36 | from airflow.io.store import attach
38 37 | from airflow.models import DAG as DAGFromModel
--------------------------------------------------------------------------------
43 42 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream
@@ -338,7 +338,7 @@ help: Use `ObjectStoragePath` from `airflow.sdk` instead.
34 34 | setup()
35 35 | from airflow.decorators import teardown
36 |-from airflow.io.path import ObjectStoragePath
37 36 | from airflow.io.storage import attach
37 36 | from airflow.io.store import attach
38 37 | from airflow.models import DAG as DAGFromModel
39 38 | from airflow.models import (
--------------------------------------------------------------------------------
@@ -350,7 +350,7 @@ help: Use `ObjectStoragePath` from `airflow.sdk` instead.
47 47 | # airflow.decorators
48 48 | teardown()
AIR311 [*] `airflow.io.storage.attach` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
AIR311 [*] `airflow.io.store.attach` 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:52:1
|
50 | # # airflow.io
@@ -366,7 +366,7 @@ help: Use `attach` from `airflow.sdk.io` instead.
34 34 | setup()
35 35 | from airflow.decorators import teardown
36 36 | from airflow.io.path import ObjectStoragePath
37 |-from airflow.io.storage import attach
37 |-from airflow.io.store import attach
38 37 | from airflow.models import DAG as DAGFromModel
39 38 | from airflow.models import (
40 39 | Connection,
@@ -391,7 +391,7 @@ AIR311 [*] `airflow.models.Connection` is removed in Airflow 3.0; It still works
help: Use `Connection` from `airflow.sdk` instead.
Unsafe fix
37 37 | from airflow.io.storage import attach
37 37 | from airflow.io.store import attach
38 38 | from airflow.models import DAG as DAGFromModel
39 39 | from airflow.models import (
40 |- Connection,
@@ -614,6 +614,8 @@ AIR311 [*] `airflow.utils.dag_parsing_context.get_parsing_context` is removed in
75 | # airflow.utils.dag_parsing_context
76 | get_parsing_context()
| ^^^^^^^^^^^^^^^^^^^
77 |
78 | from airflow.decorators.base import (
|
help: Use `get_parsing_context` from `airflow.sdk` instead.
@@ -626,3 +628,211 @@ help: Use `get_parsing_context` from `airflow.sdk` instead.
71 71 |
72 72 | # airflow.timetables.datasets
73 73 | DatasetOrTimeSchedule()
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.
--> AIR311_names.py:87:1
|
86 | # airflow.decorators.base
87 | DecoratedMappedOperator()
| ^^^^^^^^^^^^^^^^^^^^^^^
88 | DecoratedOperator()
89 | TaskDecorator()
|
help: Use `DecoratedMappedOperator` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
76 76 | get_parsing_context()
77 77 |
78 78 | from airflow.decorators.base import (
79 |- DecoratedMappedOperator,
80 79 | DecoratedOperator,
81 80 | TaskDecorator,
82 81 | get_unique_task_id,
83 82 | task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import DecoratedMappedOperator
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.decorators.base.DecoratedOperator` 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:88:1
|
86 | # airflow.decorators.base
87 | DecoratedMappedOperator()
88 | DecoratedOperator()
| ^^^^^^^^^^^^^^^^^
89 | TaskDecorator()
90 | get_unique_task_id()
|
help: Use `DecoratedOperator` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
77 77 |
78 78 | from airflow.decorators.base import (
79 79 | DecoratedMappedOperator,
80 |- DecoratedOperator,
81 80 | TaskDecorator,
82 81 | get_unique_task_id,
83 82 | task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import DecoratedOperator
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.decorators.base.TaskDecorator` 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:89:1
|
87 | DecoratedMappedOperator()
88 | DecoratedOperator()
89 | TaskDecorator()
| ^^^^^^^^^^^^^
90 | get_unique_task_id()
91 | task_decorator_factory()
|
help: Use `TaskDecorator` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
78 78 | from airflow.decorators.base import (
79 79 | DecoratedMappedOperator,
80 80 | DecoratedOperator,
81 |- TaskDecorator,
82 81 | get_unique_task_id,
83 82 | task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import TaskDecorator
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.decorators.base.get_unique_task_id` 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:90:1
|
88 | DecoratedOperator()
89 | TaskDecorator()
90 | get_unique_task_id()
| ^^^^^^^^^^^^^^^^^^
91 | task_decorator_factory()
|
help: Use `get_unique_task_id` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
79 79 | DecoratedMappedOperator,
80 80 | DecoratedOperator,
81 81 | TaskDecorator,
82 |- get_unique_task_id,
83 82 | task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import get_unique_task_id
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.decorators.base.task_decorator_factory` 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:91:1
|
89 | TaskDecorator()
90 | get_unique_task_id()
91 | task_decorator_factory()
| ^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `task_decorator_factory` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
80 80 | DecoratedOperator,
81 81 | TaskDecorator,
82 82 | get_unique_task_id,
83 |- task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import task_decorator_factory
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.models.Param` 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:97:1
|
96 | # airflow.models
97 | Param()
| ^^^^^
|
help: Use `Param` from `airflow.sdk.definitions.param` instead.
Unsafe fix
91 91 | task_decorator_factory()
92 92 |
93 93 |
94 |-from airflow.models import Param
94 |+from airflow.sdk.definitions.param import Param
95 95 |
96 96 | # airflow.models
97 97 | Param()
AIR311 [*] `airflow.sensors.base.BaseSensorOperator` 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:107:1
|
106 | # airflow.sensors.base
107 | BaseSensorOperator()
| ^^^^^^^^^^^^^^^^^^
108 | PokeReturnValue()
109 | poke_mode_only()
|
help: Use `BaseSensorOperator` from `airflow.sdk` instead.
Unsafe fix
98 98 |
99 99 |
100 100 | from airflow.sensors.base import (
101 |- BaseSensorOperator,
102 101 | PokeReturnValue,
103 102 | poke_mode_only,
104 103 | )
104 |+from airflow.sdk import BaseSensorOperator
105 105 |
106 106 | # airflow.sensors.base
107 107 | BaseSensorOperator()
AIR311 [*] `airflow.sensors.base.PokeReturnValue` 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:108:1
|
106 | # airflow.sensors.base
107 | BaseSensorOperator()
108 | PokeReturnValue()
| ^^^^^^^^^^^^^^^
109 | poke_mode_only()
|
help: Use `PokeReturnValue` from `airflow.sdk` instead.
Unsafe fix
99 99 |
100 100 | from airflow.sensors.base import (
101 101 | BaseSensorOperator,
102 |- PokeReturnValue,
103 102 | poke_mode_only,
104 103 | )
104 |+from airflow.sdk import PokeReturnValue
105 105 |
106 106 | # airflow.sensors.base
107 107 | BaseSensorOperator()
AIR311 [*] `airflow.sensors.base.poke_mode_only` 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:109:1
|
107 | BaseSensorOperator()
108 | PokeReturnValue()
109 | poke_mode_only()
| ^^^^^^^^^^^^^^
|
help: Use `poke_mode_only` from `airflow.sdk` instead.
Unsafe fix
100 100 | from airflow.sensors.base import (
101 101 | BaseSensorOperator,
102 102 | PokeReturnValue,
103 |- poke_mode_only,
104 103 | )
104 |+from airflow.sdk import poke_mode_only
105 105 |
106 106 | # airflow.sensors.base
107 107 | BaseSensorOperator()

View File

@@ -23,6 +23,7 @@ mod tests {
#[test_case(Rule::AsyncZeroSleep, Path::new("ASYNC115.py"))]
#[test_case(Rule::LongSleepNotForever, Path::new("ASYNC116.py"))]
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC210.py"))]
#[test_case(Rule::BlockingHttpCallHttpxInAsyncFunction, Path::new("ASYNC212.py"))]
#[test_case(Rule::CreateSubprocessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]

View File

@@ -0,0 +1,145 @@
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::analyze::typing::{TypeChecker, check_type, traverse_union_and_optional};
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not use blocking httpx clients.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking HTTP call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// HTTP response, negating the benefits of asynchronous programming.
///
/// Instead of using the blocking `httpx` client, use the asynchronous client.
///
/// ## Example
/// ```python
/// import httpx
///
///
/// async def fetch():
/// client = httpx.Client()
/// response = client.get(...)
/// ```
///
/// Use instead:
/// ```python
/// import httpx
///
///
/// async def fetch():
/// async with httpx.AsyncClient() as client:
/// response = await client.get(...)
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct BlockingHttpCallHttpxInAsyncFunction {
name: String,
call: String,
}
impl Violation for BlockingHttpCallHttpxInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Blocking httpx method {name}.{call}() in async context, use httpx.AsyncClient",
name = self.name,
call = self.call,
)
}
}
struct HttpxClientChecker;
impl TypeChecker for HttpxClientChecker {
fn match_annotation(
annotation: &ruff_python_ast::Expr,
semantic: &ruff_python_semantic::SemanticModel,
) -> bool {
// match base annotation directly
if semantic
.resolve_qualified_name(annotation)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["httpx", "Client"]))
{
return true;
}
// otherwise traverse any union or optional annotation
let mut found = false;
traverse_union_and_optional(
&mut |inner_expr, _| {
if semantic
.resolve_qualified_name(inner_expr)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["httpx", "Client"])
})
{
found = true;
}
},
semantic,
annotation,
);
found
}
fn match_initializer(
initializer: &ruff_python_ast::Expr,
semantic: &ruff_python_semantic::SemanticModel,
) -> bool {
let Expr::Call(ExprCall { func, .. }) = initializer else {
return false;
};
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["httpx", "Client"]))
}
}
/// ASYNC212
pub(crate) fn blocking_http_call_httpx(checker: &Checker, call: &ExprCall) {
let semantic = checker.semantic();
if !semantic.in_async_context() {
return;
}
let Some(ast::ExprAttribute { value, attr, .. }) = call.func.as_attribute_expr() else {
return;
};
let Some(name) = value.as_name_expr() else {
return;
};
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return;
};
if check_type::<HttpxClientChecker>(binding, semantic) {
if matches!(
attr.id.as_str(),
"close"
| "delete"
| "get"
| "head"
| "options"
| "patch"
| "post"
| "put"
| "request"
| "send"
| "stream"
) {
checker.report_diagnostic(
BlockingHttpCallHttpxInAsyncFunction {
name: name.id.to_string(),
call: attr.id.to_string(),
},
call.func.range(),
);
}
}
}

View File

@@ -2,6 +2,7 @@ pub(crate) use async_busy_wait::*;
pub(crate) use async_function_with_timeout::*;
pub(crate) use async_zero_sleep::*;
pub(crate) use blocking_http_call::*;
pub(crate) use blocking_http_call_httpx::*;
pub(crate) use blocking_open_call::*;
pub(crate) use blocking_process_invocation::*;
pub(crate) use blocking_sleep::*;
@@ -13,6 +14,7 @@ mod async_busy_wait;
mod async_function_with_timeout;
mod async_zero_sleep;
mod blocking_http_call;
mod blocking_http_call_httpx;
mod blocking_open_call;
mod blocking_process_invocation;
mod blocking_sleep;

View File

@@ -0,0 +1,168 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC212 Blocking httpx method client.close() in async context, use httpx.AsyncClient
--> ASYNC212.py:27:5
|
25 | async def foo():
26 | client = httpx.Client()
27 | client.close() # ASYNC212
| ^^^^^^^^^^^^
28 | client.delete() # ASYNC212
29 | client.get() # ASYNC212
|
ASYNC212 Blocking httpx method client.delete() in async context, use httpx.AsyncClient
--> ASYNC212.py:28:5
|
26 | client = httpx.Client()
27 | client.close() # ASYNC212
28 | client.delete() # ASYNC212
| ^^^^^^^^^^^^^
29 | client.get() # ASYNC212
30 | client.head() # ASYNC212
|
ASYNC212 Blocking httpx method client.get() in async context, use httpx.AsyncClient
--> ASYNC212.py:29:5
|
27 | client.close() # ASYNC212
28 | client.delete() # ASYNC212
29 | client.get() # ASYNC212
| ^^^^^^^^^^
30 | client.head() # ASYNC212
31 | client.options() # ASYNC212
|
ASYNC212 Blocking httpx method client.head() in async context, use httpx.AsyncClient
--> ASYNC212.py:30:5
|
28 | client.delete() # ASYNC212
29 | client.get() # ASYNC212
30 | client.head() # ASYNC212
| ^^^^^^^^^^^
31 | client.options() # ASYNC212
32 | client.patch() # ASYNC212
|
ASYNC212 Blocking httpx method client.options() in async context, use httpx.AsyncClient
--> ASYNC212.py:31:5
|
29 | client.get() # ASYNC212
30 | client.head() # ASYNC212
31 | client.options() # ASYNC212
| ^^^^^^^^^^^^^^
32 | client.patch() # ASYNC212
33 | client.post() # ASYNC212
|
ASYNC212 Blocking httpx method client.patch() in async context, use httpx.AsyncClient
--> ASYNC212.py:32:5
|
30 | client.head() # ASYNC212
31 | client.options() # ASYNC212
32 | client.patch() # ASYNC212
| ^^^^^^^^^^^^
33 | client.post() # ASYNC212
34 | client.put() # ASYNC212
|
ASYNC212 Blocking httpx method client.post() in async context, use httpx.AsyncClient
--> ASYNC212.py:33:5
|
31 | client.options() # ASYNC212
32 | client.patch() # ASYNC212
33 | client.post() # ASYNC212
| ^^^^^^^^^^^
34 | client.put() # ASYNC212
35 | client.request() # ASYNC212
|
ASYNC212 Blocking httpx method client.put() in async context, use httpx.AsyncClient
--> ASYNC212.py:34:5
|
32 | client.patch() # ASYNC212
33 | client.post() # ASYNC212
34 | client.put() # ASYNC212
| ^^^^^^^^^^
35 | client.request() # ASYNC212
36 | client.send() # ASYNC212
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:35:5
|
33 | client.post() # ASYNC212
34 | client.put() # ASYNC212
35 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
36 | client.send() # ASYNC212
37 | client.stream() # ASYNC212
|
ASYNC212 Blocking httpx method client.send() in async context, use httpx.AsyncClient
--> ASYNC212.py:36:5
|
34 | client.put() # ASYNC212
35 | client.request() # ASYNC212
36 | client.send() # ASYNC212
| ^^^^^^^^^^^
37 | client.stream() # ASYNC212
|
ASYNC212 Blocking httpx method client.stream() in async context, use httpx.AsyncClient
--> ASYNC212.py:37:5
|
35 | client.request() # ASYNC212
36 | client.send() # ASYNC212
37 | client.stream() # ASYNC212
| ^^^^^^^^^^^^^
38 |
39 | client.anything() # Ok
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:45:5
|
44 | async def foo(client: httpx.Client):
45 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
46 | client.anything() # Ok
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:50:5
|
49 | async def foo(client: httpx.Client | None):
50 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
51 | client.anything() # Ok
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:55:5
|
54 | async def foo(client: Optional[httpx.Client]):
55 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
56 | client.anything() # Ok
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:61:5
|
59 | async def foo():
60 | client: httpx.Client = ...
61 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
62 | client.anything() # Ok
|
ASYNC212 Blocking httpx method global_client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:69:5
|
68 | async def foo():
69 | global_client.request() # ASYNC212
| ^^^^^^^^^^^^^^^^^^^^^
70 | global_client.anything() # Ok
|

View File

@@ -22,6 +22,7 @@ mod tests {
#[test_case(Path::new("G002.py"))]
#[test_case(Path::new("G003.py"))]
#[test_case(Path::new("G004.py"))]
#[test_case(Path::new("G004_arg_order.py"))]
#[test_case(Path::new("G010.py"))]
#[test_case(Path::new("G101_1.py"))]
#[test_case(Path::new("G101_2.py"))]
@@ -48,4 +49,24 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::LoggingFString, Path::new("G004.py"))]
#[test_case(Rule::LoggingFString, Path::new("G004_arg_order.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_logging_format").join(path).as_path(),
&settings::LinterSettings {
logger_objects: vec!["logging_setup.logger".to_string()],
preview: settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,9 +1,11 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator};
use ruff_python_ast::InterpolatedStringElement;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator, StringFlags};
use ruff_python_semantic::analyze::logging;
use ruff_python_stdlib::logging::LoggingLevel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_f_string_logging_enabled;
use crate::registry::Rule;
use crate::rules::flake8_logging_format::violations::{
LoggingExcInfo, LoggingExtraAttrClash, LoggingFString, LoggingPercentFormat,
@@ -11,6 +13,87 @@ use crate::rules::flake8_logging_format::violations::{
};
use crate::{Edit, Fix};
fn logging_f_string(
checker: &Checker,
msg: &Expr,
f_string: &ast::ExprFString,
arguments: &Arguments,
msg_pos: usize,
) {
// Report the diagnostic up-front so we can attach a fix later only when preview is enabled.
let mut diagnostic = checker.report_diagnostic(LoggingFString, msg.range());
// Preview gate for the automatic fix.
if !is_fix_f_string_logging_enabled(checker.settings()) {
return;
}
// If there are existing positional arguments after the message, bail out.
// This could indicate a mistake or complex usage we shouldn't try to fix.
if arguments.args.len() > msg_pos + 1 {
return;
}
let mut format_string = String::new();
let mut args: Vec<&str> = Vec::new();
// Try to reuse the first part's quote style when building the replacement.
// Default to double quotes if we can't determine it.
let quote_str = f_string
.value
.f_strings()
.next()
.map(|f| f.flags.quote_str())
.unwrap_or("\"");
for f in f_string.value.f_strings() {
for element in &f.elements {
match element {
InterpolatedStringElement::Literal(lit) => {
// If the literal text contains a '%' placeholder, bail out: mixing
// f-string interpolation with '%' placeholders is ambiguous for our
// automatic conversion, so don't offer a fix for this case.
if lit.value.as_ref().contains('%') {
return;
}
format_string.push_str(lit.value.as_ref());
}
InterpolatedStringElement::Interpolation(interpolated) => {
if interpolated.format_spec.is_some()
|| !matches!(
interpolated.conversion,
ruff_python_ast::ConversionFlag::None
)
{
return;
}
match interpolated.expression.as_ref() {
Expr::Name(name) => {
format_string.push_str("%s");
args.push(name.id.as_str());
}
_ => return,
}
}
}
}
}
if args.is_empty() {
return;
}
let replacement = format!(
"{q}{format_string}{q}, {args}",
q = quote_str,
format_string = format_string,
args = args.join(", ")
);
let fix = Fix::safe_edit(Edit::range_replacement(replacement, msg.range()));
diagnostic.set_fix(fix);
}
/// Returns `true` if the attribute is a reserved attribute on the `logging` module's `LogRecord`
/// class.
fn is_reserved_attr(attr: &str) -> bool {
@@ -42,7 +125,7 @@ fn is_reserved_attr(attr: &str) -> bool {
}
/// Check logging messages for violations.
fn check_msg(checker: &Checker, msg: &Expr) {
fn check_msg(checker: &Checker, msg: &Expr, arguments: &Arguments, msg_pos: usize) {
match msg {
// Check for string concatenation and percent format.
Expr::BinOp(ast::ExprBinOp { op, .. }) => match op {
@@ -55,8 +138,10 @@ fn check_msg(checker: &Checker, msg: &Expr) {
_ => {}
},
// Check for f-strings.
Expr::FString(_) => {
checker.report_diagnostic_if_enabled(LoggingFString, msg.range());
Expr::FString(f_string) => {
if checker.is_rule_enabled(Rule::LoggingFString) {
logging_f_string(checker, msg, f_string, arguments, msg_pos);
}
}
// Check for .format() calls.
Expr::Call(ast::ExprCall { func, .. }) => {
@@ -168,7 +253,7 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
// G001, G002, G003, G004
let msg_pos = usize::from(matches!(logging_call_type, LoggingCallType::LogCall));
if let Some(format_arg) = call.arguments.find_argument_value("msg", msg_pos) {
check_msg(checker, format_arg);
check_msg(checker, format_arg, &call.arguments, msg_pos);
}
// G010

View File

@@ -9,6 +9,7 @@ G004 Logging statement uses f-string
| ^^^^^^^^^^^^^^^
5 | logging.log(logging.INFO, f"Hello {name}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:5:27
@@ -20,6 +21,7 @@ G004 Logging statement uses f-string
6 |
7 | _LOGGER = logging.getLogger()
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:8:14
@@ -30,6 +32,7 @@ G004 Logging statement uses f-string
9 |
10 | logging.getLogger().info(f"{name}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:10:26
@@ -41,6 +44,7 @@ G004 Logging statement uses f-string
11 |
12 | from logging import info
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:14:6
@@ -51,6 +55,7 @@ G004 Logging statement uses f-string
| ^^^^^^^^^
15 | info(f"{__name__}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:15:6
@@ -61,3 +66,156 @@ G004 Logging statement uses f-string
16 |
17 | # Don't trigger for t-strings
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:24:14
|
22 | total = 9
23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
24 | logging.info(f"{count} out of {total} files in {directory_path} checked")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:30:13
|
28 | x = 99
29 | fmt = "08d"
30 | logger.info(f"{x:{'08d'}}")
| ^^^^^^^^^^^^^^
31 | logger.info(f"{x:>10} {x:{fmt}}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:31:13
|
29 | fmt = "08d"
30 | logger.info(f"{x:{'08d'}}")
31 | logger.info(f"{x:>10} {x:{fmt}}")
| ^^^^^^^^^^^^^^^^^^^^
32 |
33 | logging.info(f"")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:33:14
|
31 | logger.info(f"{x:>10} {x:{fmt}}")
32 |
33 | logging.info(f"")
| ^^^
34 | logging.info(f"This message doesn't have any variables.")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:34:14
|
33 | logging.info(f"")
34 | logging.info(f"This message doesn't have any variables.")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 |
36 | obj = {"key": "value"}
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:37:14
|
36 | obj = {"key": "value"}
37 | logging.info(f"Object: {obj!r}")
| ^^^^^^^^^^^^^^^^^^
38 |
39 | items_count = 3
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:40:17
|
39 | items_count = 3
40 | logging.warning(f"Items: {items_count:d}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^
41 |
42 | data = {"status": "active"}
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:43:14
|
42 | data = {"status": "active"}
43 | logging.info(f"Processing {len(data)} items")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:44:14
|
42 | data = {"status": "active"}
43 | logging.info(f"Processing {len(data)} items")
44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:48:14
|
47 | result = 123
48 | logging.info(f"Calculated result: {result + 100}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 |
50 | temperature = 123
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:51:14
|
50 | temperature = 123
51 | logging.info(f"Temperature: {temperature:.1f}°C")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 |
53 | class FilePath:
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:57:14
|
55 | self.name = name
56 |
57 | logging.info(f"No changes made to {file_path.name}.")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 |
59 | user = "tron"
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:61:15
|
59 | user = "tron"
60 | balance = 123.45
61 | logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 |
63 | import logging
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:66:15
|
65 | x = 1
66 | logging.error(f"{x} -> %s", x)
| ^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
---
G004 Logging statement uses f-string
--> G004_arg_order.py:9:14
|
7 | X = 1
8 | Y = 2
9 | logger.error(f"{X} -> %s", Y)
| ^^^^^^^^^^^^
10 | logger.error(f"{Y} -> %s", X)
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004_arg_order.py:10:14
|
8 | Y = 2
9 | logger.error(f"{X} -> %s", Y)
10 | logger.error(f"{Y} -> %s", X)
| ^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -0,0 +1,291 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
---
G004 [*] Logging statement uses f-string
--> G004.py:4:14
|
3 | name = "world"
4 | logging.info(f"Hello {name}")
| ^^^^^^^^^^^^^^^
5 | logging.log(logging.INFO, f"Hello {name}")
|
help: Convert to lazy `%` formatting
Safe fix
1 1 | import logging
2 2 |
3 3 | name = "world"
4 |-logging.info(f"Hello {name}")
4 |+logging.info("Hello %s", name)
5 5 | logging.log(logging.INFO, f"Hello {name}")
6 6 |
7 7 | _LOGGER = logging.getLogger()
G004 [*] Logging statement uses f-string
--> G004.py:5:27
|
3 | name = "world"
4 | logging.info(f"Hello {name}")
5 | logging.log(logging.INFO, f"Hello {name}")
| ^^^^^^^^^^^^^^^
6 |
7 | _LOGGER = logging.getLogger()
|
help: Convert to lazy `%` formatting
Safe fix
2 2 |
3 3 | name = "world"
4 4 | logging.info(f"Hello {name}")
5 |-logging.log(logging.INFO, f"Hello {name}")
5 |+logging.log(logging.INFO, "Hello %s", name)
6 6 |
7 7 | _LOGGER = logging.getLogger()
8 8 | _LOGGER.info(f"{__name__}")
G004 [*] Logging statement uses f-string
--> G004.py:8:14
|
7 | _LOGGER = logging.getLogger()
8 | _LOGGER.info(f"{__name__}")
| ^^^^^^^^^^^^^
9 |
10 | logging.getLogger().info(f"{name}")
|
help: Convert to lazy `%` formatting
Safe fix
5 5 | logging.log(logging.INFO, f"Hello {name}")
6 6 |
7 7 | _LOGGER = logging.getLogger()
8 |-_LOGGER.info(f"{__name__}")
8 |+_LOGGER.info("%s", __name__)
9 9 |
10 10 | logging.getLogger().info(f"{name}")
11 11 |
G004 [*] Logging statement uses f-string
--> G004.py:10:26
|
8 | _LOGGER.info(f"{__name__}")
9 |
10 | logging.getLogger().info(f"{name}")
| ^^^^^^^^^
11 |
12 | from logging import info
|
help: Convert to lazy `%` formatting
Safe fix
7 7 | _LOGGER = logging.getLogger()
8 8 | _LOGGER.info(f"{__name__}")
9 9 |
10 |-logging.getLogger().info(f"{name}")
10 |+logging.getLogger().info("%s", name)
11 11 |
12 12 | from logging import info
13 13 |
G004 [*] Logging statement uses f-string
--> G004.py:14:6
|
12 | from logging import info
13 |
14 | info(f"{name}")
| ^^^^^^^^^
15 | info(f"{__name__}")
|
help: Convert to lazy `%` formatting
Safe fix
11 11 |
12 12 | from logging import info
13 13 |
14 |-info(f"{name}")
14 |+info("%s", name)
15 15 | info(f"{__name__}")
16 16 |
17 17 | # Don't trigger for t-strings
G004 [*] Logging statement uses f-string
--> G004.py:15:6
|
14 | info(f"{name}")
15 | info(f"{__name__}")
| ^^^^^^^^^^^^^
16 |
17 | # Don't trigger for t-strings
|
help: Convert to lazy `%` formatting
Safe fix
12 12 | from logging import info
13 13 |
14 14 | info(f"{name}")
15 |-info(f"{__name__}")
15 |+info("%s", __name__)
16 16 |
17 17 | # Don't trigger for t-strings
18 18 | info(t"{name}")
G004 [*] Logging statement uses f-string
--> G004.py:24:14
|
22 | total = 9
23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
24 | logging.info(f"{count} out of {total} files in {directory_path} checked")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
Safe fix
21 21 | count = 5
22 22 | total = 9
23 23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
24 |-logging.info(f"{count} out of {total} files in {directory_path} checked")
24 |+logging.info("%s out of %s files in %s checked", count, total, directory_path)
25 25 |
26 26 |
27 27 |
G004 Logging statement uses f-string
--> G004.py:30:13
|
28 | x = 99
29 | fmt = "08d"
30 | logger.info(f"{x:{'08d'}}")
| ^^^^^^^^^^^^^^
31 | logger.info(f"{x:>10} {x:{fmt}}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:31:13
|
29 | fmt = "08d"
30 | logger.info(f"{x:{'08d'}}")
31 | logger.info(f"{x:>10} {x:{fmt}}")
| ^^^^^^^^^^^^^^^^^^^^
32 |
33 | logging.info(f"")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:33:14
|
31 | logger.info(f"{x:>10} {x:{fmt}}")
32 |
33 | logging.info(f"")
| ^^^
34 | logging.info(f"This message doesn't have any variables.")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:34:14
|
33 | logging.info(f"")
34 | logging.info(f"This message doesn't have any variables.")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 |
36 | obj = {"key": "value"}
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:37:14
|
36 | obj = {"key": "value"}
37 | logging.info(f"Object: {obj!r}")
| ^^^^^^^^^^^^^^^^^^
38 |
39 | items_count = 3
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:40:17
|
39 | items_count = 3
40 | logging.warning(f"Items: {items_count:d}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^
41 |
42 | data = {"status": "active"}
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:43:14
|
42 | data = {"status": "active"}
43 | logging.info(f"Processing {len(data)} items")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:44:14
|
42 | data = {"status": "active"}
43 | logging.info(f"Processing {len(data)} items")
44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:48:14
|
47 | result = 123
48 | logging.info(f"Calculated result: {result + 100}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 |
50 | temperature = 123
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:51:14
|
50 | temperature = 123
51 | logging.info(f"Temperature: {temperature:.1f}°C")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 |
53 | class FilePath:
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:57:14
|
55 | self.name = name
56 |
57 | logging.info(f"No changes made to {file_path.name}.")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 |
59 | user = "tron"
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:61:15
|
59 | user = "tron"
60 | balance = 123.45
61 | logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 |
63 | import logging
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:66:15
|
65 | x = 1
66 | logging.error(f"{x} -> %s", x)
| ^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
---
G004 Logging statement uses f-string
--> G004_arg_order.py:9:14
|
7 | X = 1
8 | Y = 2
9 | logger.error(f"{X} -> %s", Y)
| ^^^^^^^^^^^^
10 | logger.error(f"{Y} -> %s", X)
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004_arg_order.py:10:14
|
8 | Y = 2
9 | logger.error(f"{X} -> %s", Y)
10 | logger.error(f"{Y} -> %s", X)
| ^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -327,10 +327,16 @@ impl Violation for LoggingStringConcat {
pub(crate) struct LoggingFString;
impl Violation for LoggingFString {
const FIX_AVAILABILITY: crate::FixAvailability = crate::FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Logging statement uses f-string".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Convert to lazy `%` formatting".to_string())
}
}
/// ## What it does

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -43,7 +43,7 @@ use ruff_text_size::Ranged;
/// - [Python documentation: `os.getcwd`](https://docs.python.org/3/library/os.html#os.getcwd)
/// - [Python documentation: `os.getcwdb`](https://docs.python.org/3/library/os.html#os.getcwdb)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -46,7 +46,7 @@ use crate::{FixAvailability, Violation};
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
/// - [Python documentation: `os.makedirs`](https://docs.python.org/3/library/os.html#os.makedirs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -47,7 +47,7 @@ use crate::{FixAvailability, Violation};
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
/// - [Python documentation: `os.mkdir`](https://docs.python.org/3/library/os.html#os.mkdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -1,9 +1,15 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_abspath_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_diagnostics::{Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_path_abspath_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `os.path.abspath`.
@@ -34,13 +40,18 @@ 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 `Path.resolve()` resolves symlinks, while
/// `os.path.abspath()` does not. If resolving symlinks is important, you may need to use
/// `Path.absolute()`. However, `Path.absolute()` also does not remove any `..` components in a
/// path, unlike `os.path.abspath()` and `Path.resolve()`, so if that specific combination of
/// behaviors is required, there's no existing `pathlib` alternative. See CPython issue
/// [#69200](https://github.com/python/cpython/issues/69200).
///
/// ## References
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -63,12 +74,44 @@ pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall, segments: &[&s
if segments != ["os", "path", "abspath"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"resolve()",
"path",
is_fix_os_path_abspath_enabled(checker.settings()),
OsPathAbspath,
);
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("path", 0) else {
return;
};
let arg_code = checker.locator().slice(arg.range());
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsPathAbspath, call.func.range());
if has_unknown_keywords_or_starred_expr(&call.arguments, &["path"]) {
return;
}
if !is_fix_os_path_abspath_enabled(checker.settings()) {
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}.resolve()")
} else {
format!("{binding}({arg_code}).resolve()")
};
Ok(Fix::unsafe_edits(
Edit::range_replacement(replacement, range),
[import_edit],
))
});
}

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent)
/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists)
/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getatime`](https://docs.python.org/3/library/os.path.html#os.path.getatime)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getctime`](https://docs.python.org/3/library/os.path.html#os.path.getctime)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getmtime`](https://docs.python.org/3/library/os.path.html#os.path.getmtime)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -39,7 +39,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute)
/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir)
/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file)
/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::{ExprCall, PythonVersion};
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -45,7 +45,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir)
/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -44,7 +44,7 @@ use crate::{FixAvailability, Violation};
/// ## References
/// - [Python documentation: `Path.symlink_to`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.symlink_to)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -13,7 +13,7 @@ PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
help: Replace with `Path(...).resolve()`
Safe fix
Unsafe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib

View File

@@ -13,7 +13,7 @@ PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
help: Replace with `Path(...).resolve()`
Safe fix
Unsafe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib

View File

@@ -13,7 +13,7 @@ PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
help: Replace with `Path(...).resolve()`
Safe fix
Unsafe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext

View File

@@ -13,7 +13,7 @@ PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
help: Replace with `Path(...).resolve()`
Safe fix
Unsafe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext

View File

@@ -43,7 +43,7 @@ use crate::Violation;
/// - [Python documentation: `Path.owner`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.owner)
/// - [Python documentation: `os.stat`](https://docs.python.org/3/library/os.html#os.stat)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -89,7 +89,7 @@ impl Violation for OsStat {
/// - [Python documentation: `PurePath.joinpath`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.joinpath)
/// - [Python documentation: `os.path.join`](https://docs.python.org/3/library/os.path.html#os.path.join)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -160,7 +160,7 @@ pub(crate) enum Joiner {
/// - [Python documentation: `Path.suffixes`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffixes)
/// - [Python documentation: `os.path.splitext`](https://docs.python.org/3/library/os.path.html#os.path.splitext)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -205,7 +205,7 @@ impl Violation for OsPathSplitext {
/// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open)
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -298,7 +298,7 @@ impl Violation for PyPath {
/// - [Python documentation: `Path.iterdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.iterdir)
/// - [Python documentation: `os.listdir`](https://docs.python.org/3/library/os.html#os.listdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -376,6 +376,22 @@ mod tests {
Ok(())
}
#[test_case(Rule::UnusedImport, Path::new("F401_35.py"))]
fn f401_allowed_unused_imports_top_level_module(rule_code: Rule, path: &Path) -> Result<()> {
let diagnostics = test_path(
Path::new("pyflakes").join(path).as_path(),
&LinterSettings {
pyflakes: pyflakes::settings::Settings {
allowed_unused_imports: vec!["hvplot".to_string()],
..pyflakes::settings::Settings::default()
},
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(diagnostics);
Ok(())
}
#[test]
fn f841_dummy_variable_rgx() -> Result<()> {
let diagnostics = test_path(
@@ -737,6 +753,7 @@ mod tests {
/// A re-implementation of the Pyflakes test runner.
/// Note that all tests marked with `#[ignore]` should be considered TODOs.
#[track_caller]
fn flakes(contents: &str, expected: &[Rule]) {
let contents = dedent(contents);
let source_type = PySourceType::default();

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