Compare commits

..

100 Commits

Author SHA1 Message Date
David Peter
6e83ce9e5d Address review comments 2025-05-14 20:24:11 +02:00
David Peter
d998bea5de Fix violated invariant 2025-05-14 20:24:11 +02:00
David Peter
3a76557be2 [ty] Deterministic ordering of types 2025-05-14 20:24:11 +02:00
David Peter
2a217e80ca [ty] mypy_primer: fix static-frame setup (#18103)
## Summary

Pull in https://github.com/hauntsaninja/mypy_primer/pull/169
2025-05-14 20:23:53 +02:00
Dan Parizher
030a16cb5f [flake8-simplify] Correct behavior for str.split/rsplit with maxsplit=0 (SIM905) (#18075)
Fixes #18069

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

This PR addresses a bug in the `flake8-simplify` rule `SIM905`
(split-static-string) where `str.split(maxsplit=0)` and
`str.rsplit(maxsplit=0)` produced incorrect results for empty strings or
strings starting/ending with whitespace. The fix ensures that the
linting rule's suggested replacements now align with Python's native
behavior for these specific `maxsplit=0` scenarios.

## Test Plan

1. Added new test cases to the existing
`crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py`
fixture to cover the scenarios described in issue #18069.
2.  Ran `cargo test -p ruff_linter`.
3. Verified and accepted the updated snapshots for `SIM905.py` using
`cargo insta review`. The new snapshots confirm the corrected behavior
for `maxsplit=0`.
2025-05-14 14:20:18 -04:00
Alex Waygood
0590b38214 [ty] Fix more generics-related TODOs (#18062) 2025-05-14 12:26:52 -04:00
Usul-Dev
8104b1e83b [ty] fix missing '>' in HTML anchor tags in CLI reference (#18096)
Co-authored-by: Usul <Usul-Dev@users.noreply.github.com>
2025-05-14 15:50:35 +00:00
Dhruv Manilawala
cf70c7863c Remove symlinks from the fuzz directory (#18095)
## Summary

This PR does the following:
1. Remove the symlinks from the `fuzz/` directory
2. Update `init-fuzzer.sh` script to create those symlinks
3. Update `fuzz/.gitignore` to ignore those corpus directories

## Test Plan

Initialize the fuzzer:

```sh
./fuzz/init-fuzzer.sh
```

And, run a fuzz target:

```sh
cargo +nightly fuzz run ruff_parse_simple -- -timeout=1 -only_ascii=1
```
2025-05-14 21:05:52 +05:30
Andrew Gallant
faf54c0181 ty_python_semantic: improve failed overloaded function call
The diagnostic now includes a pointer to the implementation definition
along with each possible overload.

This doesn't include information about *why* each overload failed. But
given the emphasis on concise output (since there can be *many*
unmatched overloads), it's not totally clear how to include that
additional information.

Fixes #274
2025-05-14 11:13:41 -04:00
Andrew Gallant
451c5db7a3 ty_python_semantic: move some routines to FunctionType
These are, after all, specific to function types. The methods on `Type`
are more like conveniences that return something when the type *happens*
to be a function. But defining them on `FunctionType` itself makes it
easy to call them when you have a `FunctionType` instead of a `Type`.
2025-05-14 11:13:41 -04:00
Andrew Gallant
bd5b7f415f ty_python_semantic: rejigger handling of overload error conditions
I found the previous code somewhat harder to read. Namely, a `for`
loop was being used to encode "execute zero or one times, but not
more." Which is sometimes okay, but it seemed clearer to me to use
more explicit case analysis here.

This should have no behavioral changes.
2025-05-14 11:13:41 -04:00
Andrew Gallant
0230cbac2c ty_python_semantic: update "no matching overload" diagnostic test
It looks like support for `@overload` has been added since this test was
created, so we remove the TODO and add a snippet (from #274).
2025-05-14 11:13:41 -04:00
Wei Lee
2e94d37275 [airflow] Get rid of Replacement::Name and replace them with Replacement::AutoImport for enabling auto fixing (AIR301, AIR311) (#17941)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

Similiar to https://github.com/astral-sh/ruff/pull/17941.

`Replacement::Name` was designed for linting only. Now, we also want to
fix the user code. It would be easier to replace it with a better
AutoImport struct whenever possible.

On the other hand, `AIR301` and `AIR311` contain attribute changes that
can still use a struct like `Replacement::Name`. To reduce the
confusion, I also updated it as `Replacement::AttrName`

Some of the original `Replacement::Name` has been replaced as
`Replacement::Message` as they're not directly mapping and the message
has now been moved to `help`


## Test Plan

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

The test fixtures have been updated
2025-05-14 11:10:15 -04:00
Vasco Schiavo
1e4377c9c6 [ruff] add fix safety section (RUF007) (#17755)
The PR add the `fix safety` section for rule `RUF007` (#15584 )

It seems that the fix was always marked as unsafe #14401

## Unsafety example

This first example is a little extreme. In fact, the class `Foo`
overrides the `__getitem__` method but in a very special, way. The
difference lies in the fact that `zip(letters, letters[1:])` call the
slice `letters[1:]` which is behaving weird in this case, while
`itertools.pairwise(letters)` call just `__getitem__(0), __getitem__(1),
...` and so on.

Note that the diagnostic is emitted: [playground](https://play.ruff.rs)

I don't know if we want to mention this problem, as there is a subtile
bug in the python implementation of `Foo` which make the rule unsafe.

```python
from dataclasses import dataclass
import itertools

@dataclass
class Foo:
    letters: str
    
    def __getitem__(self, index):
        return self.letters[index] + "_foo"


letters = Foo("ABCD")
zip_ = zip(letters, letters[1:])
for a, b in zip_:
    print(a, b) # A_foo B, B_foo C, C_foo D, D_foo _
    
pair = itertools.pairwise(letters)
for a, b in pair:
    print(a, b) # A_foo B_foo, B_foo C_foo, C_foo D_foo
```

This other example is much probable.
here, `itertools.pairwise` was shadowed by a costume function
[(playground)](https://play.ruff.rs)

```python
from dataclasses import dataclass
from itertools import pairwise

def pairwise(a):
    return []
    
letters = "ABCD"
zip_ = zip(letters, letters[1:])
print([(a, b) for a, b in zip_]) # [('A', 'B'), ('B', 'C'), ('C', 'D')]

pair = pairwise(letters)
print(pair) # []
```
2025-05-14 11:07:11 -04:00
Dimitri Papadopoulos Orfanos
1b4f7de840 [pyupgrade] Add resource.error as deprecated alias of OSError (UP024) (#17933)
## Summary

Partially addresses #17935.


[`resource.error`](https://docs.python.org/3/library/resource.html#resource.error)
is a deprecated alias of
[`OSError`](https://docs.python.org/3/library/exceptions.html#OSError).
> _Changed in version 3.3:_ Following [**PEP
3151**](https://peps.python.org/pep-3151/), this class was made an alias
of
[`OSError`](https://docs.python.org/3/library/exceptions.html#OSError).

Add it to the list of `OSError` aliases found by [os-error-alias
(UP024)](https://docs.astral.sh/ruff/rules/os-error-alias/#os-error-alias-up024).

## Test Plan

Sorry, I usually don't program in Rust. Could you at least point me to
the test I would need to modify?
2025-05-14 10:37:25 -04:00
Victor Hugo Gomes
9b52ae8991 [flake8-pytest-style] Don't recommend usefixtures for parametrize values in PT019 (#17650)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary
Fixes #17599.

## Test Plan

Snapshot tests.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-05-14 10:31:42 -04:00
David Peter
97d7b46936 [ty] Do not look up __init__ on instances (#18092)
## Summary

Dunder methods are never looked up on instances. We do this implicitly
in `try_call_dunder`, but the corresponding flag was missing in the
instance-construction code where we use `member_lookup_with_policy`
directly.

fixes https://github.com/astral-sh/ty/issues/322

## Test Plan

Added regression test.
2025-05-14 15:33:42 +02:00
Luke
1eab59e681 Remove double whitespace (#18090) 2025-05-14 11:20:19 +02:00
Micha Reiser
e7f97a3e4b [ty] Reduce log level of 'symbol .. (via star import) not found' log message (#18087) 2025-05-14 09:20:23 +02:00
Chandra Kiran G
d17557f0ae [ty] Fix Inconsistent casing in diagnostic (#18084) 2025-05-14 08:26:48 +02:00
Dhruv Manilawala
8cbd433a31 [ty] Add cycle handling for unpacking targets (#18078)
## Summary

This PR adds cycle handling for `infer_unpack_types` based on the
analysis in astral-sh/ty#364.

Fixes: astral-sh/ty#364

## Test Plan

Add a cycle handling test for unpacking in `cycle.md`
2025-05-13 21:27:48 +00:00
Alex Waygood
65e48cb439 [ty] Check assignments to implicit global symbols are assignable to the types declared on types.ModuleType (#18077) 2025-05-13 16:37:20 -04:00
David Peter
301d9985d8 [ty] Add benchmark for union of tuples (#18076)
## Summary

Add a micro-benchmark for the code pattern observed in
https://github.com/astral-sh/ty/issues/362.

This currently takes around 1 second on my machine.

## Test Plan

```bash
cargo bench -p ruff_benchmark -- 'ty_micro\[many_tuple' --sample-size 10
```
2025-05-13 22:14:30 +02:00
Micha Reiser
cfbb914100 Use https://ty.dev/rules when linking to the rules table (#18072) 2025-05-13 19:21:06 +02:00
Douglas Creager
fe653de3dd [ty] Infer parameter specializations of explicitly implemented generic protocols (#18054)
Follows on from (and depends on)
https://github.com/astral-sh/ruff/pull/18021.

This updates our function specialization inference to infer type
mappings from parameters that are generic protocols.

For now, this only works when the argument _explicitly_ implements the
protocol by listing it as a base class. (We end up using exactly the
same logic as for generic classes in #18021.) For this to work with
classes that _implicitly_ implement the protocol, we will have to check
the types of the protocol members (which we are not currently doing), so
that we can infer the specialization of the protocol that the class
implements.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-05-13 13:13:00 -04:00
InSync
a9f7521944 [ty] Shorten snapshot names (#18039)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-13 18:43:19 +02:00
Carl Meyer
f8890b70c3 [ty] __file__ is always a string inside a Python module (#18071)
## Summary

Understand that `__file__` is always set and a `str` when looked up as
an implicit global from a Python file we are type checking.

## Test Plan

mdtests
2025-05-13 08:20:43 -07:00
David Peter
142c1bc760 [ty] Recognize submodules in self-referential imports (#18005)
## Summary

Fix the lookup of `submodule`s in cases where the `parent` module has a
self-referential import like `from parent import submodule`. This allows
us to infer proper types for many symbols where we previously inferred
`Never`. This leads to many new false (and true) positives across the
ecosystem because the fact that we previously inferred `Never` shadowed
a lot of problems. For example, we inferred `Never` for `os.path`, which
is why we now see a lot of new diagnostics related to `os.path.abspath`
and similar.

```py
import os

reveal_type(os.path)  # previously: Never, now: <module 'os.path'>
```

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

## Ecosystem analysis

```
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┓
┃ Diagnostic ID                 ┃ Severity ┃ Removed ┃ Added ┃ Net Change ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━┩
│ call-non-callable             │ error    │       1 │     5 │         +4 │
│ call-possibly-unbound-method  │ warning  │       6 │    26 │        +20 │
│ invalid-argument-type         │ error    │      26 │    94 │        +68 │
│ invalid-assignment            │ error    │      18 │    46 │        +28 │
│ invalid-context-manager       │ error    │       9 │     4 │         -5 │
│ invalid-raise                 │ error    │       1 │     1 │          0 │
│ invalid-return-type           │ error    │       3 │    20 │        +17 │
│ invalid-super-argument        │ error    │       4 │     0 │         -4 │
│ invalid-type-form             │ error    │     573 │     0 │       -573 │
│ missing-argument              │ error    │       2 │    10 │         +8 │
│ no-matching-overload          │ error    │       0 │   715 │       +715 │
│ non-subscriptable             │ error    │       0 │    35 │        +35 │
│ not-iterable                  │ error    │       6 │     7 │         +1 │
│ possibly-unbound-attribute    │ warning  │      14 │    31 │        +17 │
│ possibly-unbound-import       │ warning  │      13 │     0 │        -13 │
│ possibly-unresolved-reference │ warning  │       0 │     8 │         +8 │
│ redundant-cast                │ warning  │       1 │     0 │         -1 │
│ too-many-positional-arguments │ error    │       2 │     0 │         -2 │
│ unknown-argument              │ error    │       2 │     0 │         -2 │
│ unresolved-attribute          │ error    │     583 │   304 │       -279 │
│ unresolved-import             │ error    │       0 │    96 │        +96 │
│ unsupported-operator          │ error    │       0 │    17 │        +17 │
│ unused-ignore-comment         │ warning  │      29 │     2 │        -27 │
├───────────────────────────────┼──────────┼─────────┼───────┼────────────┤
│ TOTAL                         │          │    1293 │  1421 │       +128 │
└───────────────────────────────┴──────────┴─────────┴───────┴────────────┘

Analysis complete. Found 23 unique diagnostic IDs.
Total diagnostics removed: 1293
Total diagnostics added: 1421
Net change: +128
```

* We see a lot of new errors (`no-matching-overload`) related to
`os.path.dirname` and other `os.path` operations because we infer `str |
None` for `__file__`, but many projects use something like
`os.path.dirname(__file__)`.
* We also see many new `unresolved-attribute` errors related to the fact
that we now infer proper module types for some imports (e.g. `import
kornia.augmentation as K`), but we don't allow implicit imports (e.g.
accessing `K.auto.operations` without also importing `K.auto`). See
https://github.com/astral-sh/ty/issues/133.
* Many false positive `invalid-type-form` are removed because we now
infer the correct type for some type expression instead of `Never`,
which is not valid in a type annotation/expression context.

## Test Plan

Added new Markdown tests
2025-05-13 16:59:11 +02:00
Alex Waygood
c0f22928bd [ty] Add a note to the diagnostic if a new builtin is used on an old Python version (#18068)
## Summary

If the user tries to use a new builtin on an old Python version, tell
them what Python version the builtin was added on, what our inferred
Python version is for their project, and what configuration settings
they can tweak to fix the error.

## Test Plan

Snapshots and screenshots:


![image](https://github.com/user-attachments/assets/767d570e-7af1-4e1f-98cf-50e4311db511)
2025-05-13 10:08:04 -04:00
Alex Waygood
5bf5f3682a [ty] Add tests for else branches of hasattr() narrowing (#18067)
## Summary

This addresses @sharkdp's post-merge review in
https://github.com/astral-sh/ruff/pull/18053#discussion_r2086190617

## Test Plan

`cargo test -p ty_python_semantic`
2025-05-13 09:57:53 -04:00
Alex Waygood
5913997c72 [ty] Improve diagnostics for assert_type and assert_never (#18050) 2025-05-13 13:00:20 +00:00
Carl Meyer
00f672a83b [ty] contribution guide (#18061)
First take on a contributing guide for `ty`. Lots of it is copied from
the existing Ruff contribution guide.

I've put this in Ruff repo, since I think a contributing guide belongs
where the code is. I also updated the Ruff contributing guide to link to
the `ty` one.

Once this is merged, we can also add a link from the `CONTRIBUTING.md`
in ty repo (which focuses on making contributions to things that are
actually in the ty repo), to this guide.

I also updated the pull request template to mention that it might be a
ty PR, and mention the `[ty]` PR title prefix.

Feel free to update/modify/merge this PR before I'm awake tomorrow.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: David Peter <mail@david-peter.de>
2025-05-13 10:55:01 +02:00
Abhijeet Prasad Bodas
68b0386007 [ty] Implement DataClassInstance protocol for dataclasses. (#18018)
Fixes: https://github.com/astral-sh/ty/issues/92

## Summary

We currently get a `invalid-argument-type` error when using
`dataclass.fields` on a dataclass, because we do not synthesize the
`__dataclass_fields__` member.

This PR fixes this diagnostic.

Note that we do not yet model the `Field` type correctly. After that is
done, we can assign a more precise `tuple[Field, ...]` type to this new
member.

## Test Plan
New mdtest.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-05-13 10:31:26 +02:00
ZGY
0ae07cdd1f [ruff_python_ast] Fix redundant visitation of test expressions in elif clause statements (#18064) 2025-05-13 07:10:23 +00:00
Douglas Creager
0fb94c052e [ty] Infer parameter specializations of generic aliases (#18021)
This updates our function specialization inference to infer type
mappings from parameters that are generic aliases, e.g.:

```py
def f[T](x: list[T]) -> T: ...

reveal_type(f(["a", "b"]))  # revealed: str
```

Though note that we're still inferring the type of list literals as
`list[Unknown]`, so for now we actually need something like the
following in our tests:

```py
def _(x: list[str]):
    reveal_type(f(x))  # revealed: str
```
2025-05-12 22:12:44 -04:00
Alex Waygood
55df9271ba [ty] Understand homogeneous tuple annotations (#17998) 2025-05-12 22:02:25 -04:00
Douglas Creager
f301931159 [ty] Induct into instances and subclasses when finding and applying generics (#18052)
We were not inducting into instance types and subclass-of types when
looking for legacy typevars, nor when apply specializations.

This addresses
https://github.com/astral-sh/ruff/pull/17832#discussion_r2081502056

```py
from __future__ import annotations
from typing import TypeVar, Any, reveal_type

S = TypeVar("S")

class Foo[T]:
    def method(self, other: Foo[S]) -> Foo[T | S]: ...  # type: ignore[invalid-return-type]

def f(x: Foo[Any], y: Foo[Any]):
    reveal_type(x.method(y))  # revealed: `Foo[Any | S]`, but should be `Foo[Any]`
```

We were not detecting that `S` made `method` generic, since we were not
finding it when searching the function signature for legacy typevars.
2025-05-12 21:53:11 -04:00
Alex Waygood
7e9b0df18a [ty] Allow classes to inherit from type[Any] or type[Unknown] (#18060) 2025-05-12 20:30:21 -04:00
Alex Waygood
41fa082414 [ty] Allow a class to inherit from an intersection if the intersection contains a dynamic type and the intersection is not disjoint from type (#18055) 2025-05-12 23:07:11 +00:00
Alex Waygood
c7b6108cb8 [ty] Narrowing for hasattr() (#18053) 2025-05-12 18:58:14 -04:00
Zanie Blue
a97e72fb5e Update reference documentation for --python-version (#18056)
Adding more detail here
2025-05-12 22:31:04 +00:00
Victor Hugo Gomes
0d6fafd0f9 [flake8-bugbear] Ignore B028 if skip_file_prefixes is present (#18047)
## Summary

Fixes #18011
2025-05-12 17:06:51 -05:00
Wei Lee
2eb2d5359b [airflow] Apply try-catch guard to all AIR3 rules (AIR3) (#17887)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

If a try-catch block guards the names, we don't raise warnings. During
this change, I discovered that some of the replacement types were
missed. Thus, I extend the fix to types other than AutoImport as well

## Test Plan

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

Test fixtures are added and updated.
2025-05-12 17:13:41 -04:00
Yunchi Pang
f549dfe39d [pylint] add fix safety section (PLW3301) (#17878)
parent: #15584 
issue: #16163

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-05-12 20:51:05 +00:00
Zanie Blue
6b64630635 Update --python to accept paths to executables in virtual environments (#17954)
## Summary

Updates the `--python` flag to accept Python executables in virtual
environments. Notably, we do not query the executable and it _must_ be
in a canonical location in a virtual environment. This is pretty naive,
but solves for the trivial case of `ty check --python .venv/bin/python3`
which will be a common mistake (and `ty check --python $(which python)`)

I explored this while trying to understand Python discovery in ty in
service of https://github.com/astral-sh/ty/issues/272, I'm not attached
to it, but figure it's worth sharing.

As an alternative, we can add more variants to the
`SearchPathValidationError` and just improve the _error_ message, i.e.,
by hinting that this looks like a virtual environment and suggesting the
concrete alternative path they should provide. We'll probably want to do
that for some other cases anyway (e.g., `3.13` as described in the
linked issue)

This functionality is also briefly mentioned in
https://github.com/astral-sh/ty/issues/193

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

## Test Plan

e.g.,

```
uv run ty check --python .venv/bin/python3
```

needs test coverage still
2025-05-12 15:39:04 -05:00
Yunchi Pang
d545b5bfd2 [pylint] add fix safety section (PLE4703) (#17824)
This PR adds a fix safety section in comment for rule PLE4703.

parent: #15584 
impl was introduced at #970 (couldn't find newer PRs sorry!)
2025-05-12 16:27:54 -04:00
Marcus Näslund
b2d9f59937 [ruff] Implement a recursive check for RUF060 (#17976)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
The existing implementation of RUF060 (InEmptyCollection) is not
recursive, meaning that although set([]) results in an empty collection,
the existing code fails it because set is taking an argument.

The updated implementation allows set and frozenset to take empty
collection as positional argument (which results in empty
set/frozenset).

## Test Plan

Added test cases for recursive cases + updated snapshot (see RUF060.py).

---------

Co-authored-by: Marcus Näslund <marcus.naslund@kognity.com>
2025-05-12 16:17:13 -04:00
Victor Hugo Gomes
d7ef01401c [flake8-use-pathlib] PTH* suppress diagnostic for all os.* functions that have the dir_fd parameter (#17968)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

Fixes #17776.

This PR also handles all other `PTH*` rules that don't support file
descriptors.

## Test Plan

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

Update existing tests.
2025-05-12 16:11:56 -04:00
Victor Hugo Gomes
c9031ce59f [refurb] Mark autofix as safe only for number literals in FURB116 (#17692)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary
We can only guarantee the safety of the autofix for number literals, all
other cases may change the runtime behaviour of the program or introduce
a syntax error. For the cases reported in the issue that would result in
a syntax error, I disabled the autofix.

Follow-up of #17661. 

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

## Test Plan

Snapshot tests.
<!-- How was it tested? -->
2025-05-12 16:08:12 -04:00
Victor Hugo Gomes
138ab91def [flake8-simplify] Fix SIM905 autofix for rsplit creating a reversed list literal (#18045)
## Summary

Fixes #18042
2025-05-12 14:53:08 -05:00
Ibraheem Ahmed
550b8be552 Avoid initializing progress bars early (#18049)
## Summary

Resolves https://github.com/astral-sh/ty/issues/324.
2025-05-12 15:07:55 -04:00
Douglas Creager
bdccb37b4a [ty] Apply function specialization to all overloads (#18020)
Function literals have an optional specialization, which is applied to
the parameter/return type annotations lazily when the function's
signature is requested. We were previously only applying this
specialization to the final overload of an overloaded function.

This manifested most visibly for `list.__add__`, which has an overloaded
definition in the typeshed:


b398b83631/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi (L1069-L1072)

Closes https://github.com/astral-sh/ty/issues/314
2025-05-12 13:48:54 -04:00
Charlie Marsh
3ccc0edfe4 Add comma to panic message (#18048)
## Summary

Consistent with other variants of this, separate the conditional clause.
2025-05-12 11:52:55 -04:00
Victor Hugo Gomes
6b3ff6f5b8 [flake8-pie] Mark autofix for PIE804 as unsafe if the dictionary contains comments (#18046)
## Summary

Fixes #18036
2025-05-12 10:16:59 -05:00
Shunsuke Shibayama
6f8f7506b4 [ty] fix infinite recursion bug in is_disjoint_from (#18043)
## Summary

I found this bug while working on #18041. The following code leads to
infinite recursion.

```python
from ty_extensions import is_disjoint_from, static_assert, TypeOf

class C:
    @property
    def prop(self) -> int:
        return 1

static_assert(not is_disjoint_from(int, TypeOf[C.prop]))
```

The cause is a trivial missing binding in `is_disjoint_from`. This PR
fixes the bug and adds a test case (this is a simple fix and may not
require a new test case?).

## Test Plan

A new test case is added to
`mdtest/type_properties/is_disjoint_from.md`.
2025-05-12 09:44:00 -04:00
Micha Reiser
797eb70904 disable jemalloc on android (#18033) 2025-05-12 14:41:00 +02:00
Micha Reiser
be6ec613db [ty] Fix incorrect type of src.root in documentation (#18040) 2025-05-12 12:28:14 +00:00
Micha Reiser
fcd858e0c8 [ty] Refine message for why a rule is enabled (#18038) 2025-05-12 13:31:42 +02:00
Micha Reiser
d944a1397e [ty] Remove brackets around option names (#18037) 2025-05-12 11:16:03 +00:00
renovate[bot]
d3f3d92df3 Update pre-commit dependencies (#18025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:31:23 +02:00
renovate[bot]
38c00dfad5 Update docker/build-push-action action to v6.16.0 (#18030)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:26:41 +02:00
renovate[bot]
d6280c5aea Update docker/login-action action to v3.4.0 (#18031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:26:22 +02:00
renovate[bot]
a34240a3f0 Update taiki-e/install-action digest to 83254c5 (#18022)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:26:03 +02:00
renovate[bot]
b86c7bbf7c Update cargo-bins/cargo-binstall action to v1.12.4 (#18023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:25:28 +02:00
renovate[bot]
d7c54ba8c4 Update Rust crate ctrlc to v3.4.7 (#18027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:25:08 +02:00
renovate[bot]
c38d6e8045 Update Rust crate clap to v4.5.38 (#18026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:24:40 +02:00
renovate[bot]
2bfd7b1816 Update Rust crate jiff to v0.2.13 (#18029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:24:16 +02:00
renovate[bot]
c1cfb43bf0 Update Rust crate getrandom to v0.3.3 (#18028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:23:49 +02:00
renovate[bot]
99555b775c Update dependency ruff to v0.11.9 (#18024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 22:03:55 -04:00
Yunchi Pang
b398b83631 [pylint] add fix safety section (PLW1514) (#17932)
parent #15584 
fix was made unsafe at #8928
2025-05-11 12:25:07 -05:00
Rogdham
bc7b30364d python_stdlib: update for 3.14 (#18014)
## Summary

Added version 3.14 to the script generating the `known_stdlib.rs` file.

Rebuilt the known stdlibs with latest version (2025.5.10) of [stdlibs
Python lib](https://pypi.org/project/stdlibs/) (which added support for
3.14.0b1).

_Note: Python 3.14 is now in [feature
freeze](https://peps.python.org/pep-0745/) so the modules in stdlib
should be stable._

_See also: #15506_

## Test Plan

The following command has been run. Using for tests the `compression`
module which been introduced with Python 3.14.
```sh
ruff check --no-cache --select I001 --target-version py314 --fix
```

With ruff 0.11.9:
```python
import base64
import datetime

import compression

print(base64, compression, datetime)
```

With this PR:
```python
import base64
import compression
import datetime   

print(base64, compression, datetime)
```
2025-05-11 11:25:54 -05:00
Vasco Schiavo
5792ed15da [ruff] add fix safety section (RUF033) (#17760)
This PR adds the fix safety section for rule `RUF033`
(https://github.com/astral-sh/ruff/issues/15584 ).
2025-05-11 11:15:15 -05:00
Yunchi Pang
8845a13efb [pylint] add fix safety section (PLC0414) (#17802)
This PR adds a fix safety section in comment for rule `PLC0414`.

parent: #15584 
discussion: #6294
2025-05-11 11:01:26 -05:00
Alex Waygood
669855d2b5 [ty] Remove unused variants from various Known* enums (#18015)
## Summary

`KnownClass::Range`, `KnownInstanceType::Any` and `ClassBase::any()` are
no longer used or useful: all our tests pass with them removed.
`KnownModule::Abc` _is_ now used outside of tests, however, so I removed
the `#[allow(dead_code)]` branch above that variant.

## Test Plan

`cargo test -p ty_python_semantic`
2025-05-11 11:18:55 +01:00
Alex Waygood
ff7ebecf89 [ty] Remove generic types from the daily property test run (for now) (#18004) 2025-05-11 09:27:27 +00:00
Zanie Blue
7e8ba2b68e [ty] Remove vestigial pyvenv.cfg creation in mdtest (#18006)
Following #17991, removes some of
https://github.com/astral-sh/ruff/pull/17222 which is no longer strictly
necessary. I don't actually think it's that ugly to have around? no
strong feelings on retaining it or not.
2025-05-10 20:52:49 +00:00
Zanie Blue
0bb8cbdf07 [ty] Do not allow invalid virtual environments from discovered .venv or VIRTUAL_ENV (#18003)
Follow-up to https://github.com/astral-sh/ruff/pull/17991 ensuring we do
not allow detection of system environments when the origin is
`VIRTUAL_ENV` or a discovered `.venv` directory — i.e., those always
require a `pyvenv.cfg` file.
2025-05-10 20:36:12 +00:00
Zanie Blue
2923c55698 [ty] Add test coverage for PythonEnvironment::System variants (#17996)
Adds test coverage for https://github.com/astral-sh/ruff/pull/17991,
which includes some minor refactoring of the virtual environment test
infrastructure.

I tried to minimize stylistic changes, but there are still a few because
I was a little confused by the setup. I could see this evolving more in
the future, as I don't think the existing model can capture all the test
coverage I'm looking for.
2025-05-10 20:28:15 +00:00
Zanie Blue
316e406ca4 [ty] Add basic support for non-virtual Python environments (#17991)
This adds basic support for non-virtual Python environments by accepting
a directory without a `pyvenv.cfg` which allows existing, subsequent
site-packages discovery logic to succeed. We can do better here in the
long-term, by adding more eager validation (for error messages) and
parsing the Python version from the discovered site-packages directory
(which isn't relevant yet, because we don't use the discovered Python
version from virtual environments as the default `--python-version` yet
either).

Related

- https://github.com/astral-sh/ty/issues/265
- https://github.com/astral-sh/ty/issues/193

You can review this commit by commit if it makes you happy.

I tested this manually; I think refactoring the test setup is going to
be a bit more invasive so I'll stack it on top (see
https://github.com/astral-sh/ruff/pull/17996).

```
❯ uv run ty check --python /Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none/ -vv example
2025-05-09 12:06:33.685911 DEBUG Version: 0.0.0-alpha.7 (f9c4c8999 2025-05-08)
2025-05-09 12:06:33.685987 DEBUG Architecture: aarch64, OS: macos, case-sensitive: case-insensitive
2025-05-09 12:06:33.686002 DEBUG Searching for a project in '/Users/zb/workspace/ty'
2025-05-09 12:06:33.686123 DEBUG Resolving requires-python constraint: `>=3.8`
2025-05-09 12:06:33.686129 DEBUG Resolved requires-python constraint to: 3.8
2025-05-09 12:06:33.686142 DEBUG Project without `tool.ty` section: '/Users/zb/workspace/ty'
2025-05-09 12:06:33.686147 DEBUG Searching for a user-level configuration at `/Users/zb/.config/ty/ty.toml`
2025-05-09 12:06:33.686156 INFO Defaulting to python-platform `darwin`
2025-05-09 12:06:33.68636 INFO Python version: Python 3.8, platform: darwin
2025-05-09 12:06:33.686375 DEBUG Adding first-party search path '/Users/zb/workspace/ty'
2025-05-09 12:06:33.68638 DEBUG Using vendored stdlib
2025-05-09 12:06:33.686634 DEBUG Discovering site-packages paths from sys-prefix `/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none` (`--python` argument')
2025-05-09 12:06:33.686667 DEBUG Attempting to parse virtual environment metadata at '/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none/pyvenv.cfg'
2025-05-09 12:06:33.686671 DEBUG Searching for site-packages directory in `sys.prefix` path `/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none`
2025-05-09 12:06:33.686702 DEBUG Resolved site-packages directories for this environment are: ["/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none/lib/python3.10/site-packages"]
2025-05-09 12:06:33.686706 DEBUG Adding site-packages search path '/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none/lib/python3.10/site-packages'
...

❯ uv run ty check --python /tmp -vv example
2025-05-09 15:36:10.819416 DEBUG Version: 0.0.0-alpha.7 (f9c4c8999 2025-05-08)
2025-05-09 15:36:10.819708 DEBUG Architecture: aarch64, OS: macos, case-sensitive: case-insensitive
2025-05-09 15:36:10.820118 DEBUG Searching for a project in '/Users/zb/workspace/ty'
2025-05-09 15:36:10.821652 DEBUG Resolving requires-python constraint: `>=3.8`
2025-05-09 15:36:10.821667 DEBUG Resolved requires-python constraint to: 3.8
2025-05-09 15:36:10.8217 DEBUG Project without `tool.ty` section: '/Users/zb/workspace/ty'
2025-05-09 15:36:10.821888 DEBUG Searching for a user-level configuration at `/Users/zb/.config/ty/ty.toml`
2025-05-09 15:36:10.822072 INFO Defaulting to python-platform `darwin`
2025-05-09 15:36:10.822439 INFO Python version: Python 3.8, platform: darwin
2025-05-09 15:36:10.822773 DEBUG Adding first-party search path '/Users/zb/workspace/ty'
2025-05-09 15:36:10.822929 DEBUG Using vendored stdlib
2025-05-09 15:36:10.829872 DEBUG Discovering site-packages paths from sys-prefix `/tmp` (`--python` argument')
2025-05-09 15:36:10.829911 DEBUG Attempting to parse virtual environment metadata at '/private/tmp/pyvenv.cfg'
2025-05-09 15:36:10.829917 DEBUG Searching for site-packages directory in `sys.prefix` path `/private/tmp`
ty failed
  Cause: Invalid search path settings
  Cause: Failed to discover the site-packages directory: Failed to search the `lib` directory of the Python installation at `sys.prefix` path `/private/tmp` for `site-packages`
```
2025-05-10 20:17:47 +00:00
Micha Reiser
5ecd560c6f Link to the rules.md in the ty repository (#17979) 2025-05-10 11:40:40 +01:00
Max Mynter
b765dc48e9 Skip S608 for expressionless f-strings (#17999) 2025-05-10 11:37:58 +01:00
David Peter
cd1d906ffa [ty] Silence false positives for PEP-695 ParamSpec annotations (#18001)
## Summary

Suppress false positives for uses of PEP-695 `ParamSpec` in `Callable`
annotations:
```py
from typing_extensions import Callable

def f[**P](c: Callable[P, int]):
    pass
```

addresses a comment here:
https://github.com/astral-sh/ty/issues/157#issuecomment-2859284721

## Test Plan

Adapted Markdown tests
2025-05-10 11:59:25 +02:00
Abhijeet Prasad Bodas
235b74a310 [ty] Add more tests for NamedTuples (#17975)
## Summary

Add more tests and TODOs for `NamedTuple` support, based on the typing
spec: https://typing.python.org/en/latest/spec/namedtuples.html

## Test Plan

This PR adds new tests.
2025-05-10 10:46:08 +02:00
Brent Westbrook
40fd52dde0 Exclude broken symlinks from meson-python ecosystem check (#17993)
Summary
--

This should resolve the formatter ecosystem errors we've been seeing
lately. https://github.com/mesonbuild/meson-python/pull/728 added the
links, which I think are intentionally broken for testing purposes.

Test Plan
--

Ecosystem check on this PR
2025-05-09 16:59:00 -04:00
Carl Meyer
fd1eb3d801 add test for typing_extensions.Self (#17995)
Using `typing_extensions.Self` already worked, but we were lacking a
test for it.
2025-05-09 20:29:13 +00:00
omahs
882a1a702e Fix typos (#17988)
Fix typos

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-05-09 14:57:14 -04:00
Max Mynter
b4a1ebdfe3 [semantic-syntax-tests] IrrefutableCasePattern, SingleStarredAssignment, WriteToDebug, InvalidExpression (#17748)
Re: #17526 

## Summary

Add integration test for semantic syntax for `IrrefutableCasePattern`,
`SingleStarredAssignment`, `WriteToDebug`, and `InvalidExpression`.

## Notes
- Following @ntBre's suggestion, I will keep the test coming in batches
like this over the next few days in separate PRs to keep the review load
per PR manageable while also not spamming too many.

- I did not add a test for `del __debug__` which is one of the examples
in `crates/ruff_python_parser/src/semantic_errors.rs:1051`.
For python version `<= 3.8` there is no error and for `>=3.9` the error
is not `WriteToDebug` but `SyntaxError: cannot delete __debug__ on
Python 3.9 (syntax was removed in 3.9)`.

- The `blacken-docs` bypass is necessary because otherwise the test does
not pass pre-commit checks; but we want to check for this faulty syntax.

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

## Test Plan
This is a test.
2025-05-09 14:54:05 -04:00
Brent Westbrook
7a48477c67 [ty] Add a warning about pre-release status to the CLI (#17983)
Summary
--

This was suggested on Discord, I hope this is roughly what we had in
mind. I took the message from the ty README, but I'm more than happy to
update it. Otherwise I just tried to mimic the appearance of the `ruff
analyze graph` warning (although I'm realizing now the whole text is
bold for ruff).

Test Plan
--

New warnings in the CLI tests. I thought this might be undesirable but
it looks like uv did the same thing
(https://github.com/astral-sh/uv/pull/6166).


![image](https://github.com/user-attachments/assets/e5e56a49-02ab-4c5f-9c38-716e4008d6e6)
2025-05-09 13:42:36 -04:00
Andrew Gallant
346e82b572 ty_python_semantic: add union type context to function call type errors
This context gets added only when calling a function through a union
type.
2025-05-09 13:40:51 -04:00
Andrew Gallant
5ea3a52c8a ty_python_semantic: report all union diagnostic
This makes one very simple change: we report all call binding
errors from each union variant.

This does result in duplicate-seeming diagnostics. For example,
when two union variants are invalid for the same reason.
2025-05-09 13:40:51 -04:00
Andrew Gallant
90272ad85a ty_python_semantic: add snapshot tests for existing union function type diagnostics
This is just capturing the status quo so that we can better see the
changes. I took these tests from the (now defunct) PR #17959.
2025-05-09 13:40:51 -04:00
Ibraheem Ahmed
e9da1750a1 Add progress bar for ty check (#17965)
## Summary

Adds a simple progress bar for the `ty check` CLI command. The style is
taken from uv, and like uv the bar is always shown - for smaller
projects it is fast enough that it isn't noticeable. We could
alternatively hide it completely based on some heuristic for the number
of files, or only show it after some amount of time.

I also disabled it when `--watch` is passed, cancelling inflight checks
was leading to zombie progress bars. I think we can fix this by using
[`MultiProgress`](https://docs.rs/indicatif/latest/indicatif/struct.MultiProgress.html)
and managing all the bars globally, but I left that out for now.

Resolves https://github.com/astral-sh/ty/issues/98.
2025-05-09 13:32:27 -04:00
Wei Lee
25e13debc0 [airflow] extend AIR311 rules (#17913)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

* `airflow.models.Connection` → `airflow.sdk.Connection`
* `airflow.models.Variable` → `airflow.sdk.Variable`

## Test Plan

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

The test fixtures has been updated (see the first commit for easier
review)
2025-05-09 13:08:37 -04:00
InSync
249a852a6e [ty] Document nearly all lints (#17981) 2025-05-09 18:06:56 +01:00
Andrew Gallant
861ef2504e ty: add more snapshot updates 2025-05-09 12:42:14 -04:00
Andrew Gallant
b71ef8a26e ruff_db: completely rip lint: prefix out
This does a deeper removal of the `lint:` prefix by removing the
`DiagnosticId::as_str` method and replacing it with `as_concise_str`. We
remove the associated error type and simplify the `Display` impl for
`DiagnosticId` as well.

This turned out to catch a `lint:` that was still in the diagnostic
output: the part that says why a lint is enabled.
2025-05-09 12:42:14 -04:00
Andrew Gallant
50c780fc8b ty: switch to use annotate-snippets ID functionality
We just set the ID on the `Message` and it just does what we want in
this case. I think I didn't do this originally because I was trying to
preserve the existing rendering? I'm not sure. I might have just missed
this method.
2025-05-09 12:42:14 -04:00
Andrew Gallant
244ea27d5f ruff_db: a small tweak to remove empty message case
In a subsequent commit, we're going to start using `annotate-snippets`'s
functionality for diagnostic IDs in the rendering. As part of doing
that, I wanted to remove this special casing of an empty message. I did
that independently to see what, if anything, would change. (The changes
look fine to me. They'll be tweaked again in the next commit along with
a bunch of others.)
2025-05-09 12:42:14 -04:00
Andrew Gallant
2c4cbb6e29 ty: get rid of lint: prefix in ID for diagnostic rendering
In #289, we seem to have consensus that this prefix isn't really pulling
its weight.

Ref #289
2025-05-09 12:42:14 -04:00
Alex Waygood
d1bb10a66b [ty] Understand classes that inherit from subscripted Protocol[] as generic (#17832) 2025-05-09 17:39:15 +01:00
319 changed files with 9493 additions and 3197 deletions

View File

@@ -1,8 +1,9 @@
<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:
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?
- 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?
-->

View File

@@ -40,7 +40,7 @@ jobs:
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -79,7 +79,7 @@ jobs:
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Build and push by digest
id: build
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: .
platforms: ${{ matrix.platform }}
@@ -131,7 +131,7 @@ jobs:
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -169,7 +169,7 @@ jobs:
steps:
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -231,7 +231,7 @@ jobs:
${{ env.TAG_PATTERNS }}
- name: Build and push
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -276,7 +276,7 @@ jobs:
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -239,11 +239,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -297,11 +297,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -324,7 +324,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -407,11 +407,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -437,7 +437,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
uses: cargo-bins/cargo-binstall@13f9d60d5358393bf14644dba56d9f123bc5d595 # v1.12.4
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -692,7 +692,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
- uses: cargo-bins/cargo-binstall@13f9d60d5358393bf14644dba56d9f123bc5d595 # v1.12.4
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -908,7 +908,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
with:
tool: cargo-codspeed

View File

@@ -69,7 +69,7 @@ jobs:
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@4b15cf3b07db69db67bbfaebfffb2a8a28040933" \
--from="git+https://github.com/hauntsaninja/mypy_primer@968b2b61c05f84462d6fcc78d2f5205bbb8b98c2" \
mypy_primer \
--repo ruff \
--type-checker ty \

View File

@@ -80,7 +80,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
rev: v0.11.9
hooks:
- id: ruff-format
- id: ruff
@@ -98,7 +98,7 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.6.0
rev: v1.7.0
hooks:
- id: zizmor

View File

@@ -2,6 +2,11 @@
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
> [!NOTE]
>
> This guide is for Ruff. If you're looking to contribute to ty, please see [the ty contributing
> guide](https://github.com/astral-sh/ruff/blob/main/crates/ty/CONTRIBUTING.md).
## The Basics
Ruff welcomes contributions in the form of pull requests.

58
Cargo.lock generated
View File

@@ -334,9 +334,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.37"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
"clap_derive",
@@ -344,9 +344,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.37"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
@@ -409,7 +409,7 @@ version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
dependencies = [
"nix",
"nix 0.29.0",
"terminfo",
"thiserror 2.0.12",
"which",
@@ -681,11 +681,11 @@ dependencies = [
[[package]]
name = "ctrlc"
version = "3.4.6"
version = "3.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
dependencies = [
"nix",
"nix 0.30.1",
"windows-sys 0.59.0",
]
@@ -1057,9 +1057,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"js-sys",
@@ -1539,9 +1539,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.12"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd"
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
@@ -1554,9 +1554,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.12"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300"
checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
dependencies = [
"proc-macro2",
"quote",
@@ -1880,6 +1880,18 @@ dependencies = [
"libc",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
@@ -2458,7 +2470,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.2",
"getrandom 0.3.3",
]
[[package]]
@@ -3138,7 +3150,7 @@ version = "0.11.9"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.2",
"getrandom 0.3.3",
"js-sys",
"log",
"ruff_formatter",
@@ -3219,6 +3231,12 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc-stable-hash"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08"
[[package]]
name = "rustix"
version = "0.38.44"
@@ -3625,7 +3643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
dependencies = [
"fastrand",
"getrandom 0.3.2",
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.2",
"windows-sys 0.59.0",
@@ -3984,6 +4002,7 @@ dependencies = [
"crossbeam",
"ctrlc",
"filetime",
"indicatif",
"insta",
"insta-cmd",
"jiff",
@@ -4140,6 +4159,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-stable-hash",
"salsa",
"serde",
"smallvec",
@@ -4167,7 +4187,7 @@ version = "0.0.0"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.2",
"getrandom 0.3.3",
"js-sys",
"log",
"ruff_db",
@@ -4352,7 +4372,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
"getrandom 0.3.3",
"js-sys",
"rand 0.9.1",
"uuid-macro-internal",

View File

@@ -125,6 +125,7 @@ rand = { version = "0.9.0" }
rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
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 = "7edce6e248f35c8114b4b021cdb474a3fb2813b3" }
schemars = { version = "0.8.16" }

View File

@@ -255,7 +255,7 @@ indent-width = 4
target-version = "py39"
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"]
ignore = []

View File

@@ -84,7 +84,7 @@ dist = true
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
tikv-jemallocator = { workspace = true }
[lints]

View File

@@ -15,6 +15,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
not(target_os = "windows"),
not(target_os = "openbsd"),
not(target_os = "aix"),
not(target_os = "android"),
any(
target_arch = "x86_64",
target_arch = "aarch64",

View File

@@ -301,6 +301,62 @@ fn benchmark_many_string_assignments(criterion: &mut Criterion) {
});
}
fn benchmark_many_tuple_assignments(criterion: &mut Criterion) {
setup_rayon();
criterion.bench_function("ty_micro[many_tuple_assignments]", |b| {
b.iter_batched_ref(
|| {
// This is a micro benchmark, but it is effectively identical to a code sample
// observed in https://github.com/astral-sh/ty/issues/362
setup_micro_case(
r#"
def flag() -> bool:
return True
t = ()
if flag():
t += (1,)
if flag():
t += (2,)
if flag():
t += (3,)
if flag():
t += (4,)
if flag():
t += (5,)
if flag():
t += (6,)
if flag():
t += (7,)
if flag():
t += (8,)
if flag():
t += (9,)
if flag():
t += (10,)
if flag():
t += (11,)
# Perform some kind of operation on the union type
print(1 in t)
"#,
)
},
|case| {
let Case { db, .. } = case;
let result = db.check().unwrap();
assert_eq!(result.len(), 0);
},
BatchSize::SmallInput,
);
});
}
criterion_group!(check_file, benchmark_cold, benchmark_incremental);
criterion_group!(micro, benchmark_many_string_assignments);
criterion_group!(
micro,
benchmark_many_string_assignments,
benchmark_many_tuple_assignments
);
criterion_main!(check_file, micro);

View File

@@ -2,7 +2,6 @@ use std::{fmt::Formatter, sync::Arc};
use render::{FileResolver, Input};
use ruff_source_file::{SourceCode, SourceFile};
use thiserror::Error;
use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange};
@@ -613,40 +612,19 @@ impl DiagnosticId {
code.split_once(':').map(|(_, rest)| rest)
}
/// Returns `true` if this `DiagnosticId` matches the given name.
/// Returns a concise description of this diagnostic ID.
///
/// ## Examples
/// ```
/// use ruff_db::diagnostic::DiagnosticId;
///
/// assert!(DiagnosticId::Io.matches("io"));
/// assert!(DiagnosticId::lint("test").matches("lint:test"));
/// assert!(!DiagnosticId::lint("test").matches("test"));
/// ```
pub fn matches(&self, expected_name: &str) -> bool {
match self.as_str() {
Ok(id) => id == expected_name,
Err(DiagnosticAsStrError::Category { category, name }) => expected_name
.strip_prefix(category)
.and_then(|prefix| prefix.strip_prefix(":"))
.is_some_and(|rest| rest == name),
}
}
pub fn as_str(&self) -> Result<&str, DiagnosticAsStrError> {
Ok(match self {
/// Note that this doesn't include the lint's category. It
/// only includes the lint's name.
pub fn as_str(&self) -> &str {
match self {
DiagnosticId::Panic => "panic",
DiagnosticId::Io => "io",
DiagnosticId::InvalidSyntax => "invalid-syntax",
DiagnosticId::Lint(name) => {
return Err(DiagnosticAsStrError::Category {
category: "lint",
name: name.as_str(),
})
}
DiagnosticId::Lint(name) => name.as_str(),
DiagnosticId::RevealedType => "revealed-type",
DiagnosticId::UnknownRule => "unknown-rule",
})
}
}
pub fn is_invalid_syntax(&self) -> bool {
@@ -654,26 +632,9 @@ impl DiagnosticId {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
pub enum DiagnosticAsStrError {
/// The id can't be converted to a string because it belongs to a sub-category.
#[error("id from a sub-category: {category}:{name}")]
Category {
/// The id's category.
category: &'static str,
/// The diagnostic id in this category.
name: &'static str,
},
}
impl std::fmt::Display for DiagnosticId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.as_str() {
Ok(name) => f.write_str(name),
Err(DiagnosticAsStrError::Category { category, name }) => {
write!(f, "{category}:{name}")
}
}
write!(f, "{}", self.as_str())
}
}

View File

@@ -140,7 +140,6 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
/// both.)
#[derive(Debug)]
struct Resolved<'a> {
id: String,
diagnostics: Vec<ResolvedDiagnostic<'a>>,
}
@@ -152,14 +151,12 @@ impl<'a> Resolved<'a> {
for sub in &diag.inner.subs {
diagnostics.push(ResolvedDiagnostic::from_sub_diagnostic(resolver, sub));
}
let id = diag.inner.id.to_string();
Resolved { id, diagnostics }
Resolved { diagnostics }
}
/// Creates a value that is amenable to rendering directly.
fn to_renderable(&self, context: usize) -> Renderable<'_> {
Renderable {
id: &self.id,
diagnostics: self
.diagnostics
.iter()
@@ -177,6 +174,7 @@ impl<'a> Resolved<'a> {
#[derive(Debug)]
struct ResolvedDiagnostic<'a> {
severity: Severity,
id: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
}
@@ -197,20 +195,11 @@ impl<'a> ResolvedDiagnostic<'a> {
ResolvedAnnotation::new(path, &diagnostic_source, ann)
})
.collect();
let message = if diag.inner.message.as_str().is_empty() {
diag.inner.id.to_string()
} else {
// TODO: See the comment on `Renderable::id` for
// a plausible better idea than smushing the ID
// into the diagnostic message.
format!(
"{id}: {message}",
id = diag.inner.id,
message = diag.inner.message.as_str(),
)
};
let id = Some(diag.inner.id.to_string());
let message = diag.inner.message.as_str().to_string();
ResolvedDiagnostic {
severity: diag.inner.severity,
id,
message,
annotations,
}
@@ -233,6 +222,7 @@ impl<'a> ResolvedDiagnostic<'a> {
.collect();
ResolvedDiagnostic {
severity: diag.inner.severity,
id: None,
message: diag.inner.message.as_str().to_string(),
annotations,
}
@@ -299,6 +289,7 @@ impl<'a> ResolvedDiagnostic<'a> {
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
RenderableDiagnostic {
severity: self.severity,
id: self.id.as_deref(),
message: &self.message,
snippets_by_input,
}
@@ -378,20 +369,6 @@ impl<'a> ResolvedAnnotation<'a> {
/// renderable value. This is usually the lifetime of `Resolved`.
#[derive(Debug)]
struct Renderable<'r> {
// TODO: This is currently unused in the rendering logic below. I'm not
// 100% sure yet where I want to put it, but I like what `rustc` does:
//
// error[E0599]: no method named `sub_builder` <..snip..>
//
// I believe in order to do this, we'll need to patch it in to
// `ruff_annotate_snippets` though. We leave it here for now with that plan
// in mind.
//
// (At time of writing, 2025-03-13, we currently render the diagnostic
// ID into the main message of the parent diagnostic. We don't use this
// specific field to do that though.)
#[expect(dead_code)]
id: &'r str,
diagnostics: Vec<RenderableDiagnostic<'r>>,
}
@@ -400,6 +377,12 @@ struct Renderable<'r> {
struct RenderableDiagnostic<'r> {
/// The severity of the diagnostic.
severity: Severity,
/// The ID of the diagnostic. The ID can usually be used on the CLI or in a
/// config file to change the severity of a lint.
///
/// An ID is always present for top-level diagnostics and always absent for
/// sub-diagnostics.
id: Option<&'r str>,
/// The message emitted with the diagnostic, before any snippets are
/// rendered.
message: &'r str,
@@ -420,7 +403,11 @@ impl RenderableDiagnostic<'_> {
.iter()
.map(|snippet| snippet.to_annotate(path))
});
level.title(self.message).snippets(snippets)
let mut message = level.title(self.message);
if let Some(id) = self.id {
message = message.id(id);
}
message.snippets(snippets)
}
}
@@ -799,7 +786,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -823,7 +810,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
warning: lint:test-diagnostic: main diagnostic message
warning[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -843,7 +830,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
info: lint:test-diagnostic: main diagnostic message
info[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -870,7 +857,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -889,7 +876,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -910,7 +897,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> non-ascii:5:1
|
3 | ΔΔΔΔΔΔΔΔΔΔΔΔ
@@ -929,7 +916,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> non-ascii:2:2
|
1 | ☃☃☃☃☃☃☃☃☃☃☃☃
@@ -953,7 +940,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
4 | dog
@@ -970,7 +957,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
5 | elephant
@@ -985,7 +972,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1002,7 +989,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:11:1
|
9 | inchworm
@@ -1019,7 +1006,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
1 | aardvark
@@ -1052,7 +1039,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1096,7 +1083,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1121,7 +1108,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1149,7 +1136,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1177,7 +1164,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1202,7 +1189,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1233,7 +1220,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1271,7 +1258,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> spacey-animals:8:1
|
7 | dog
@@ -1288,7 +1275,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> spacey-animals:12:1
|
11 | gorilla
@@ -1306,7 +1293,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> spacey-animals:13:1
|
11 | gorilla
@@ -1346,7 +1333,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> spacey-animals:3:1
|
3 | beetle
@@ -1375,7 +1362,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1412,7 +1399,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1449,7 +1436,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1477,7 +1464,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1513,7 +1500,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1552,7 +1539,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1600,7 +1587,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1636,7 +1623,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1659,7 +1646,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1679,7 +1666,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1699,7 +1686,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:4
|
3 | canary
@@ -1721,7 +1708,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:4
|
3 | canary
@@ -1753,7 +1740,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:4:1
|
2 | beetle
@@ -1782,7 +1769,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:4:1
|
2 | beetle
@@ -1813,7 +1800,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1848,7 +1835,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1876,7 +1863,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1908,7 +1895,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:3
|
3 | canary
@@ -1930,7 +1917,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:3
|
3 | canary
@@ -1963,7 +1950,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:8:1
|
6 | finch
@@ -2003,7 +1990,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
5 | elephant
@@ -2047,7 +2034,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> fruits:1:1
|
1 | apple
@@ -2082,7 +2069,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error: lint:test-diagnostic: main diagnostic message
error[test-diagnostic]: main diagnostic message
--> animals:11:1
|
11 | kangaroo

View File

@@ -179,7 +179,7 @@ fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut
let id = format!("{name_key}--{}", arg.get_id());
output.push_str(&format!("<dt id=\"{id}\">"));
output.push_str(&format!(
"<a href=\"#{id}\"<code>{}</code></a>",
"<a href=\"#{id}\"><code>{}</code></a>",
arg.get_id().to_string().to_uppercase(),
));
output.push_str("</dt>");

View File

@@ -134,13 +134,13 @@ impl Set {
fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) {
let header_level = if parents.is_empty() { "###" } else { "####" };
let _ = writeln!(output, "{header_level} [`{name}`]");
let _ = writeln!(output, "{header_level} `{name}`");
output.push('\n');
if let Some(deprecated) = &field.deprecated {
output.push_str("!!! warning \"Deprecated\"\n");
output.push_str(" This option has been deprecated");
output.push_str("> [!WARN] \"Deprecated\"\n");
output.push_str("> This option has been deprecated");
if let Some(since) = deprecated.since {
write!(output, " in {since}").unwrap();

View File

@@ -92,7 +92,7 @@ impl std::fmt::Display for IndentStyle {
}
}
/// The visual width of a indentation.
/// The visual width of an indentation.
///
/// Determines the visual width of a tab character (`\t`) and the number of
/// spaces per indent when using [`IndentStyle::Space`].
@@ -207,7 +207,7 @@ pub trait FormatOptions {
/// What's the max width of a line. Defaults to 80.
fn line_width(&self) -> LineWidth;
/// Derives the print options from the these format options
/// Derives the print options from these format options
fn as_print_options(&self) -> PrinterOptions;
}

View File

@@ -24,9 +24,6 @@ from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.datasets import DatasetAliasEvent
from airflow.hooks.base_hook import BaseHook
from airflow.operators.subdag import SubDagOperator
from airflow.providers.mysql.datasets import mysql
from airflow.providers.postgres.datasets import postgres
from airflow.providers.trino.datasets import trino
from airflow.secrets.local_filesystem import LocalFilesystemBackend
from airflow.sensors.base_sensor_operator import BaseSensorOperator
from airflow.triggers.external_task import TaskStateTrigger
@@ -78,14 +75,6 @@ BaseHook()
# airflow.operators.subdag.*
SubDagOperator()
# airflow.providers.mysql
mysql.sanitize_uri
# airflow.providers.postgres
postgres.sanitize_uri
# airflow.providers.trino
trino.sanitize_uri
# airflow.secrets
# get_connection
@@ -155,3 +144,18 @@ should_hide_value_for_key
from airflow.operators.python import get_current_context
get_current_context()
# airflow.providers.mysql
from airflow.providers.mysql.datasets.mysql import sanitize_uri
sanitize_uri
# airflow.providers.postgres
from airflow.providers.postgres.datasets.postgres import sanitize_uri
sanitize_uri
# airflow.providers.trino
from airflow.providers.trino.datasets.trino import sanitize_uri
sanitize_uri

View File

@@ -1,17 +1,8 @@
from __future__ import annotations
try:
from airflow.sdk import Asset
from airflow.assets.manager import AssetManager
except ModuleNotFoundError:
from airflow.datasets import Dataset as Asset
from airflow.datasets.manager import DatasetManager as AssetManager
Asset
try:
from airflow.sdk import Asset
except ModuleNotFoundError:
from airflow import datasets
Asset = datasets.Dataset
asset = Asset()
AssetManager()

View File

@@ -0,0 +1,8 @@
from __future__ import annotations
try:
from airflow.providers.http.operators.http import HttpOperator
except ModuleNotFoundError:
from airflow.operators.http_operator import SimpleHttpOperator as HttpOperator
HttpOperator()

View File

@@ -13,6 +13,10 @@ from airflow.decorators import dag, setup, task, task_group, teardown
from airflow.io.path import ObjectStoragePath
from airflow.io.storage import attach
from airflow.models import DAG as DAGFromModel
from airflow.models import (
Connection,
Variable,
)
from airflow.models.baseoperator import chain, chain_linear, cross_downstream
from airflow.models.baseoperatorlink import BaseOperatorLink
from airflow.models.dag import DAG as DAGFromDag
@@ -42,7 +46,9 @@ ObjectStoragePath()
attach()
# airflow.models
Connection()
DAGFromModel()
Variable()
# airflow.models.baseoperator
chain()

View File

@@ -0,0 +1,8 @@
from __future__ import annotations
try:
from airflow.sdk import Asset
except ModuleNotFoundError:
from airflow.datasets import Dataset as Asset
Asset()

View File

@@ -0,0 +1,8 @@
from __future__ import annotations
try:
from airflow.providers.standard.triggers.file import FileTrigger
except ModuleNotFoundError:
from airflow.triggers.file import FileTrigger
FileTrigger()

View File

@@ -166,3 +166,6 @@ query60 = f"""
foo
FROM ({user_input}) raw
"""
# https://github.com/astral-sh/ruff/issues/17967
query61 = f"SELECT * FROM table" # skip expressionless f-strings

View File

@@ -25,3 +25,11 @@ warnings.warn(
# some comments here
source = None # no trailing comma
)
# https://github.com/astral-sh/ruff/issues/18011
warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),))
# trigger diagnostic if `skip_file_prefixes` is present and set to the default value
warnings.warn("test", skip_file_prefixes=())
_my_prefixes = ("this","that")
warnings.warn("test", skip_file_prefixes = _my_prefixes)

View File

@@ -28,3 +28,14 @@ abc(a=1, **{'a': c}, **{'b': c}) # PIE804
# Some values need to be parenthesized.
abc(foo=1, **{'bar': (bar := 1)}) # PIE804
abc(foo=1, **{'bar': (yield 1)}) # PIE804
# https://github.com/astral-sh/ruff/issues/18036
# The autofix for this is unsafe due to the comments inside the dictionary.
foo(
**{
# Comment 1
"x": 1.0,
# Comment 2
"y": 2.0,
}
)

View File

@@ -12,3 +12,38 @@ def test_xxx(_fixture): # Error arg
def test_xxx(*, _fixture): # Error kwonly
pass
# https://github.com/astral-sh/ruff/issues/17599
@pytest.mark.parametrize("_foo", [1, 2, 3])
def test_thingy(_foo): # Ok defined in parametrize
pass
@pytest.mark.parametrize(
"_test_input,_expected",
[("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(_test_input, _expected): # OK defined in parametrize
pass
@pytest.mark.parametrize("_foo", [1, 2, 3])
def test_thingy2(_foo, _bar): # Error _bar is not defined in parametrize
pass
@pytest.mark.parametrize(["_foo", "_bar"], [1, 2, 3])
def test_thingy3(_foo, _bar): # OK defined in parametrize
pass
@pytest.mark.parametrize(("_foo"), [1, 2, 3])
def test_thingy4(_foo, _bar): # Error _bar is not defined in parametrize
pass
@pytest.mark.parametrize([" _foo", " _bar "], [1, 2, 3])
def test_thingy5(_foo, _bar): # OK defined in parametrize
pass
x = "other"
@pytest.mark.parametrize(x, [1, 2, 3])
def test_thingy5(_foo, _bar): # known false negative, we don't try to resolve variables
pass

View File

@@ -31,7 +31,7 @@ no_sep = None
" a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
"".split() # []
"""
"""
""".split() # []
" ".split() # []
"/abc/".split() # ["/abc/"]
@@ -73,7 +73,7 @@ r"\n " "\n".split() # [r"\n"]
# negatives
# invalid values should not cause panic
# invalid values should not cause panic
"a,b,c,d".split(maxsplit="hello")
"a,b,c,d".split(maxsplit=-"hello")
@@ -106,3 +106,27 @@ b"TesT.WwW.ExamplE.CoM".split(b".")
'''itemC'''
"'itemD'"
""".split()
# https://github.com/astral-sh/ruff/issues/18042
print("a,b".rsplit(","))
print("a,b,c".rsplit(",", 1))
# https://github.com/astral-sh/ruff/issues/18069
print("".split(maxsplit=0))
print("".split(sep=None, maxsplit=0))
print(" ".split(maxsplit=0))
print(" ".split(sep=None, maxsplit=0))
print(" x ".split(maxsplit=0))
print(" x ".split(sep=None, maxsplit=0))
print(" x ".split(maxsplit=0))
print(" x ".split(sep=None, maxsplit=0))
print("".rsplit(maxsplit=0))
print("".rsplit(sep=None, maxsplit=0))
print(" ".rsplit(maxsplit=0))
print(" ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))

View File

@@ -9,3 +9,7 @@ extensions_dir = "./extensions"
glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"))
list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp")))
search("*.png")
# if `dir_fd` is set, suppress the diagnostic
glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1)
list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1))

View File

@@ -87,3 +87,20 @@ def bar(x: int):
os.rename("src", "dst", src_dir_fd=3, dst_dir_fd=4)
os.rename("src", "dst", src_dir_fd=3)
os.rename("src", "dst", dst_dir_fd=4)
# if `dir_fd` is set, suppress the diagnostic
os.readlink(p, dir_fd=1)
os.stat(p, dir_fd=2)
os.unlink(p, dir_fd=3)
os.remove(p, dir_fd=4)
os.rmdir(p, dir_fd=5)
os.mkdir(p, dir_fd=6)
os.chmod(p, dir_fd=7)
# `chmod` can also receive a file descriptor in the first argument
os.chmod(8)
os.chmod(x)
# if `src_dir_fd` or `dst_dir_fd` are set, suppress the diagnostic
os.replace("src", "dst", src_dir_fd=1, dst_dir_fd=2)
os.replace("src", "dst", src_dir_fd=1)
os.replace("src", "dst", dst_dir_fd=2)

View File

@@ -144,14 +144,14 @@ def f():
def f():
# make sure that `tmp` is not deleted
tmp = 1; result = [] # commment should be protected
tmp = 1; result = [] # comment should be protected
for i in range(10):
result.append(i + 1) # PERF401
def f():
# make sure that `tmp` is not deleted
result = []; tmp = 1 # commment should be protected
result = []; tmp = 1 # comment should be protected
for i in range(10):
result.append(i + 1) # PERF401

View File

@@ -6,14 +6,16 @@ from .mmap import error
raise error
# Testing the modules
import socket, mmap, select
import socket, mmap, select, resource
raise socket.error
raise mmap.error
raise select.error
raise resource.error
raise socket.error()
raise mmap.error(1)
raise select.error(1, 2)
raise resource.error(1, "strerror", "filename")
raise socket.error(
1,
@@ -30,6 +32,9 @@ raise error(1)
from select import error
raise error(1, 2)
from resource import error
raise error(1, "strerror", "filename")
# Testing the names
raise EnvironmentError
raise IOError

View File

@@ -1,3 +1,6 @@
import datetime
import sys
num = 1337
def return_num() -> int:
@@ -10,6 +13,7 @@ print(bin(num)[2:]) # FURB116
print(oct(1337)[2:]) # FURB116
print(hex(1337)[2:]) # FURB116
print(bin(1337)[2:]) # FURB116
print(bin(+1337)[2:]) # FURB116
print(bin(return_num())[2:]) # FURB116 (no autofix)
print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
@@ -22,3 +26,19 @@ print(hex(0x1337)[3:])
# float and complex numbers should be ignored
print(bin(1.0)[2:])
print(bin(3.14j)[2:])
d = datetime.datetime.now(tz=datetime.UTC)
# autofix is display-only
print(bin(d)[2:])
# no autofix for Python 3.11 and earlier, as it introduces a syntax error
print(bin(len("xyz").numerator)[2:])
# autofix is display-only
print(bin({0: 1}[0].numerator)[2:])
# no autofix for Python 3.11 and earlier, as it introduces a syntax error
print(bin(ord("\\").numerator)[2:])
print(hex(sys
.maxunicode)[2:])
# for negatives numbers autofix is display-only
print(bin(-1)[2:])

View File

@@ -19,6 +19,9 @@ b'c' in b""
b"a" in bytearray()
b"a" in bytes()
1 in frozenset()
1 in set(set())
2 in frozenset([])
'' in set("")
# OK
1 in [2]
@@ -35,3 +38,7 @@ b'c' in b"x"
b"a" in bytearray([2])
b"a" in bytes("a", "utf-8")
1 in frozenset("c")
1 in set(set((1,2)))
1 in set(set([1]))
'' in {""}
frozenset() in {frozenset()}

View File

@@ -1116,6 +1116,95 @@ mod tests {
PythonVersion::PY310,
"InvalidStarExpression"
)]
#[test_case(
"irrefutable_case_pattern_wildcard",
"
match value:
case _:
pass
case 1:
pass
",
PythonVersion::PY310,
"IrrefutableCasePattern"
)]
#[test_case(
"irrefutable_case_pattern_capture",
"
match value:
case irrefutable:
pass
case 1:
pass
",
PythonVersion::PY310,
"IrrefutableCasePattern"
)]
#[test_case(
"single_starred_assignment",
"*a = [1, 2, 3, 4]",
PythonVersion::PY310,
"SingleStarredAssignment"
)]
#[test_case(
"write_to_debug",
"
__debug__ = False
",
PythonVersion::PY310,
"WriteToDebug"
)]
#[test_case(
"write_to_debug_in_function_param",
"
def process(__debug__):
pass
",
PythonVersion::PY310,
"WriteToDebug"
)]
#[test_case(
"write_to_debug_class_type_param",
"
class Generic[__debug__]:
pass
",
PythonVersion::PY312,
"WriteToDebug"
)]
#[test_case(
"invalid_expression_yield_in_type_param",
"
type X[T: (yield 1)] = int
",
PythonVersion::PY312,
"InvalidExpression"
)]
#[test_case(
"invalid_expression_yield_in_type_alias",
"
type Y = (yield 1)
",
PythonVersion::PY312,
"InvalidExpression"
)]
#[test_case(
"invalid_expression_walrus_in_return_annotation",
"
def f[T](x: int) -> (y := 3): return x
",
PythonVersion::PY312,
"InvalidExpression"
)]
#[test_case(
"invalid_expression_yield_from_in_base_class",
"
class C[T]((yield from [object])):
pass
",
PythonVersion::PY312,
"InvalidExpression"
)]
fn test_semantic_errors(
name: &str,
contents: &str,

View File

@@ -8,13 +8,20 @@ use ruff_python_semantic::SemanticModel;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Replacement {
// There's no replacement or suggestion other than removal
None,
Name(&'static str),
// The attribute name of a class has been changed.
AttrName(&'static str),
// Additional information. Used when there's replacement but they're not direct mapping.
Message(&'static str),
// Symbols updated in Airflow 3 with replacement
// e.g., `airflow.datasets.Dataset` to `airflow.sdk.Asset`
AutoImport {
module: &'static str,
name: &'static str,
},
// Symbols updated in Airflow 3 with only module changed. Used when we want to match multiple names.
// e.g., `airflow.configuration.as_dict | get` to `airflow.configuration.conf.as_dict | get`
SourceModuleMoved {
module: &'static str,
name: String,
@@ -45,7 +52,8 @@ pub(crate) enum ProviderReplacement {
pub(crate) fn is_guarded_by_try_except(
expr: &Expr,
replacement: &Replacement,
module: &str,
name: &str,
semantic: &SemanticModel,
) -> bool {
match expr {
@@ -63,7 +71,7 @@ pub(crate) fn is_guarded_by_try_except(
if !suspended_exceptions.contains(Exceptions::ATTRIBUTE_ERROR) {
return false;
}
try_block_contains_undeprecated_attribute(try_node, replacement, semantic)
try_block_contains_undeprecated_attribute(try_node, module, name, semantic)
}
Expr::Name(ExprName { id, .. }) => {
let Some(binding_id) = semantic.lookup_symbol(id.as_str()) else {
@@ -89,7 +97,7 @@ pub(crate) fn is_guarded_by_try_except(
{
return false;
}
try_block_contains_undeprecated_import(try_node, replacement)
try_block_contains_undeprecated_import(try_node, module, name)
}
_ => false,
}
@@ -100,12 +108,10 @@ pub(crate) fn is_guarded_by_try_except(
/// member is being accessed from the non-deprecated location?
fn try_block_contains_undeprecated_attribute(
try_node: &StmtTry,
replacement: &Replacement,
module: &str,
name: &str,
semantic: &SemanticModel,
) -> bool {
let Replacement::AutoImport { module, name } = replacement else {
return false;
};
let undeprecated_qualified_name = {
let mut builder = QualifiedNameBuilder::default();
for part in module.split('.') {
@@ -122,10 +128,7 @@ fn try_block_contains_undeprecated_attribute(
/// Given an [`ast::StmtTry`] node, does the `try` branch of that node
/// contain any [`ast::StmtImportFrom`] nodes that indicate the airflow
/// member is being imported from the non-deprecated location?
fn try_block_contains_undeprecated_import(try_node: &StmtTry, replacement: &Replacement) -> bool {
let Replacement::AutoImport { module, name } = replacement else {
return false;
};
fn try_block_contains_undeprecated_import(try_node: &StmtTry, module: &str, name: &str) -> bool {
let mut import_searcher = ImportSearcher::new(module, name);
import_searcher.visit_body(&try_node.body);
import_searcher.found_import

View File

@@ -46,9 +46,12 @@ mod tests {
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_sqlite.py"))]
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_zendesk.py"))]
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_standard.py"))]
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_try.py"))]
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_args.py"))]
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_names.py"))]
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_try.py"))]
#[test_case(Rule::Airflow3SuggestedToMoveToProvider, Path::new("AIR312.py"))]
#[test_case(Rule::Airflow3SuggestedToMoveToProvider, Path::new("AIR312_try.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,5 +1,5 @@
use crate::importer::ImportRequest;
use crate::rules::airflow::helpers::ProviderReplacement;
use crate::rules::airflow::helpers::{is_guarded_by_try_except, ProviderReplacement};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{Expr, ExprAttribute};
@@ -937,13 +937,17 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
ranged.range(),
);
if let ProviderReplacement::AutoImport {
module,
name,
provider: _,
version: _,
} = replacement
{
let semantic = checker.semantic();
if let Some((module, name)) = match &replacement {
ProviderReplacement::AutoImport { module, name, .. } => Some((module, *name)),
ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => {
Some((module, name.as_str()))
}
_ => None,
} {
if is_guarded_by_try_except(expr, module, name, semantic) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, name),
@@ -954,6 +958,5 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
Ok(Fix::safe_edits(import_edit, [replacement_edit]))
});
}
checker.report_diagnostic(diagnostic);
}

View File

@@ -57,28 +57,27 @@ impl Violation for Airflow3Removal {
} = self;
match replacement {
Replacement::None
| Replacement::Name(_)
| Replacement::AttrName(_)
| Replacement::Message(_)
| Replacement::AutoImport { module: _, name: _ }
| Replacement::SourceModuleMoved { module: _, name: _ } => {
format!("`{deprecated}` is removed in Airflow 3.0")
}
Replacement::Message(message) => {
format!("`{deprecated}` is removed in Airflow 3.0; {message}")
}
}
}
fn fix_title(&self) -> Option<String> {
let Airflow3Removal { replacement, .. } = self;
match replacement {
Replacement::Name(name) => Some(format!("Use `{name}` instead")),
Replacement::None => None,
Replacement::AttrName(name) => Some(format!("Use `{name}` instead")),
Replacement::Message(message) => Some((*message).to_string()),
Replacement::AutoImport { module, name } => {
Some(format!("Use `{module}.{name}` instead"))
}
Replacement::SourceModuleMoved { module, name } => {
Some(format!("Use `{module}.{name}` instead"))
}
_ => None,
}
}
}
@@ -278,22 +277,22 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) {
let replacement = match *qualname.segments() {
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
"dataset_factories" => Replacement::Name("asset_factories"),
"dataset_uri_handlers" => Replacement::Name("asset_uri_handlers"),
"dataset_factories" => Replacement::AttrName("asset_factories"),
"dataset_uri_handlers" => Replacement::AttrName("asset_uri_handlers"),
"dataset_to_openlineage_converters" => {
Replacement::Name("asset_to_openlineage_converters")
Replacement::AttrName("asset_to_openlineage_converters")
}
_ => return,
},
["airflow", "lineage", "hook", "DatasetLineageInfo"] => match attr.as_str() {
"dataset" => Replacement::Name("asset"),
"dataset" => Replacement::AttrName("asset"),
_ => return,
},
_ => return,
};
// Create the `Fix` first to avoid cloning `Replacement`.
let fix = if let Replacement::Name(name) = replacement {
let fix = if let Replacement::AttrName(name) = replacement {
Some(Fix::safe_edit(Edit::range_replacement(
name.to_string(),
attr.range(),
@@ -466,52 +465,52 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
let replacement = match qualname.segments() {
["airflow", "datasets", "manager", "DatasetManager"] => match attr.as_str() {
"register_dataset_change" => Replacement::Name("register_asset_change"),
"create_datasets" => Replacement::Name("create_assets"),
"notify_dataset_created" => Replacement::Name("notify_asset_created"),
"notify_dataset_changed" => Replacement::Name("notify_asset_changed"),
"notify_dataset_alias_created" => Replacement::Name("notify_asset_alias_created"),
"register_dataset_change" => Replacement::AttrName("register_asset_change"),
"create_datasets" => Replacement::AttrName("create_assets"),
"notify_dataset_created" => Replacement::AttrName("notify_asset_created"),
"notify_dataset_changed" => Replacement::AttrName("notify_asset_changed"),
"notify_dataset_alias_created" => Replacement::AttrName("notify_asset_alias_created"),
_ => return,
},
["airflow", "lineage", "hook", "HookLineageCollector"] => match attr.as_str() {
"create_dataset" => Replacement::Name("create_asset"),
"add_input_dataset" => Replacement::Name("add_input_asset"),
"add_output_dataset" => Replacement::Name("add_output_asset"),
"collected_datasets" => Replacement::Name("collected_assets"),
"create_dataset" => Replacement::AttrName("create_asset"),
"add_input_dataset" => Replacement::AttrName("add_input_asset"),
"add_output_dataset" => Replacement::AttrName("add_output_asset"),
"collected_datasets" => Replacement::AttrName("collected_assets"),
_ => return,
},
["airflow", "providers", "amazon", "auth_manager", "aws_auth_manager", "AwsAuthManager"] => {
match attr.as_str() {
"is_authorized_dataset" => Replacement::Name("is_authorized_asset"),
"is_authorized_dataset" => Replacement::AttrName("is_authorized_asset"),
_ => return,
}
}
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
"initialize_providers_dataset_uri_resources" => {
Replacement::Name("initialize_providers_asset_uri_resources")
Replacement::AttrName("initialize_providers_asset_uri_resources")
}
_ => return,
},
["airflow", "secrets", "local_filesystem", "LocalFilesystemBackend"] => match attr.as_str()
{
"get_connections" => Replacement::Name("get_connection"),
"get_connections" => Replacement::AttrName("get_connection"),
_ => return,
},
["airflow", "datasets", ..] | ["airflow", "Dataset"] => match attr.as_str() {
"iter_datasets" => Replacement::Name("iter_assets"),
"iter_dataset_aliases" => Replacement::Name("iter_asset_aliases"),
"iter_datasets" => Replacement::AttrName("iter_assets"),
"iter_dataset_aliases" => Replacement::AttrName("iter_asset_aliases"),
_ => return,
},
segments => {
if is_airflow_secret_backend(segments) {
match attr.as_str() {
"get_conn_uri" => Replacement::Name("get_conn_value"),
"get_connections" => Replacement::Name("get_connection"),
"get_conn_uri" => Replacement::AttrName("get_conn_value"),
"get_connections" => Replacement::AttrName("get_connection"),
_ => return,
}
} else if is_airflow_hook(segments) {
match attr.as_str() {
"get_connections" => Replacement::Name("get_connection"),
"get_connections" => Replacement::AttrName("get_connection"),
_ => return,
}
} else {
@@ -520,7 +519,7 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
}
};
// Create the `Fix` first to avoid cloning `Replacement`.
let fix = if let Replacement::Name(name) = replacement {
let fix = if let Replacement::AttrName(name) = replacement {
Some(Fix::safe_edit(Edit::range_replacement(
name.to_string(),
attr.range(),
@@ -566,12 +565,12 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
let replacement = match qualified_name.segments() {
// airflow.PY\d{1,2}
["airflow", "PY36" | "PY37" | "PY38" | "PY39" | "PY310" | "PY311" | "PY312"] => {
Replacement::Name("sys.version_info")
Replacement::Message("Use `sys.version_info` instead")
}
// airflow.api_connexion.security
["airflow", "api_connexion", "security", "requires_access"] => {
Replacement::Name("airflow.api_connexion.security.requires_access_*")
Replacement::Message("Use `airflow.api_connexion.security.requires_access_*` instead")
}
["airflow", "api_connexion", "security", "requires_access_dataset"] => {
Replacement::AutoImport {
@@ -626,9 +625,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
["airflow", "datasets", "DatasetAliasEvent"] => Replacement::None,
// airflow.hooks
["airflow", "hooks", "base_hook", "BaseHook"] => {
Replacement::Name("airflow.hooks.base.BaseHook")
}
["airflow", "hooks", "base_hook", "BaseHook"] => Replacement::AutoImport {
module: "airflow.hooks.base",
name: "BaseHook",
},
// airflow.lineage.hook
["airflow", "lineage", "hook", "DatasetLineageInfo"] => Replacement::AutoImport {
@@ -664,9 +664,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
},
// airflow.notifications
["airflow", "notifications", "basenotifier", "BaseNotifier"] => {
Replacement::Name("airflow.sdk.BaseNotifier")
}
["airflow", "notifications", "basenotifier", "BaseNotifier"] => Replacement::AutoImport {
module: "airflow.sdk",
name: "BaseNotifier",
},
// airflow.operators
["airflow", "operators", "subdag", ..] => {
@@ -691,7 +692,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
// airflow.sensors
["airflow", "sensors", "base_sensor_operator", "BaseSensorOperator"] => {
Replacement::Name("airflow.sdk.bases.sensor.BaseSensorOperator")
Replacement::AutoImport {
module: "airflow.sdk.bases.sensor",
name: "BaseSensorOperator",
}
}
// airflow.timetables
@@ -720,23 +724,36 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
// airflow.utils.dates
["dates", "date_range"] => Replacement::None,
["dates", "days_ago"] => Replacement::Name("pendulum.today('UTC').add(days=-N, ...)"),
["dates", "days_ago"] => {
Replacement::Message("Use `pendulum.today('UTC').add(days=-N, ...)` instead")
}
["dates", "parse_execution_date" | "round_time" | "scale_time_units" | "infer_time_unit"] => {
Replacement::None
}
// airflow.utils.file
["file", "TemporaryDirectory"] => Replacement::Name("tempfile.TemporaryDirectory"),
["file", "mkdirs"] => Replacement::Name("pathlib.Path({path}).mkdir"),
["file", "TemporaryDirectory"] => Replacement::AutoImport {
module: "tempfile",
name: "TemporaryDirectory",
},
["file", "mkdirs"] => Replacement::Message("Use `pathlib.Path({path}).mkdir` instead"),
// airflow.utils.helpers
["helpers", "chain"] => Replacement::Name("airflow.sdk.chain"),
["helpers", "cross_downstream"] => Replacement::Name("airflow.sdk.cross_downstream"),
["helpers", "chain"] => Replacement::AutoImport {
module: "airflow.sdk",
name: "chain",
},
["helpers", "cross_downstream"] => Replacement::AutoImport {
module: "airflow.sdk",
name: "cross_downstream",
},
// TODO: update it as SourceModuleMoved
// airflow.utils.log.secrets_masker
["log", "secrets_masker"] => {
Replacement::Name("airflow.sdk.execution_time.secrets_masker")
}
["log", "secrets_masker"] => Replacement::AutoImport {
module: "airflow.sdk.execution_time",
name: "secrets_masker",
},
// airflow.utils.state
["state", "SHUTDOWN" | "terminating_states"] => Replacement::None,
@@ -751,18 +768,20 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
// airflow.www
// TODO: www has been removed
["airflow", "www", "auth", "has_access"] => {
Replacement::Name("airflow.www.auth.has_access_*")
Replacement::Message("Use `airflow.www.auth.has_access_*` instead")
}
["airflow", "www", "auth", "has_access_dataset"] => Replacement::AutoImport {
module: "airflow.www.auth",
name: "has_access_asset",
},
["airflow", "www", "utils", "get_sensitive_variables_fields"] => {
Replacement::Name("airflow.utils.log.secrets_masker.get_sensitive_variables_fields")
}
["airflow", "www", "utils", "should_hide_value_for_key"] => {
Replacement::Name("airflow.utils.log.secrets_masker.should_hide_value_for_key")
}
["airflow", "www", "utils", "get_sensitive_variables_fields"] => Replacement::AutoImport {
module: "airflow.utils.log.secrets_masker",
name: "get_sensitive_variables_fields",
},
["airflow", "www", "utils", "should_hide_value_for_key"] => Replacement::AutoImport {
module: "airflow.utils.log.secrets_masker",
name: "should_hide_value_for_key",
},
// airflow.providers.amazon
["airflow", "providers", "amazon", "aws", "datasets", "s3", rest] => match *rest {
@@ -774,9 +793,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.providers.amazon.aws.assets.s3",
name: "convert_asset_to_openlineage",
},
"sanitize_uri" => {
Replacement::Name("airflow.providers.amazon.aws.assets.s3.sanitize_uri")
}
"sanitize_uri" => Replacement::AutoImport {
module: "airflow.providers.amazon.aws.assets.s3",
name: "sanitize_uri",
},
_ => return,
},
["airflow", "providers", "amazon", "aws", "auth_manager", "avp", "entities", "AvpEntities", "DATASET"] => {
@@ -797,9 +817,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.providers.common.io.assets.file",
name: "convert_asset_to_openlineage",
},
"sanitize_uri" => {
Replacement::Name("airflow.providers.common.io.assets.file.sanitize_uri")
}
"sanitize_uri" => Replacement::AutoImport {
module: "airflow.providers.common.io.assets.file",
name: "sanitize_uri",
},
_ => return,
},
@@ -826,20 +847,28 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.providers.google.assets.gcs",
name: "convert_asset_to_openlineage",
},
["gcs", "sanitize_uri"] => {
Replacement::Name("airflow.providers.google.assets.gcs.sanitize_uri")
}
["gcs", "sanitize_uri"] => Replacement::AutoImport {
module: "airflow.providers.google.assets.gcs",
name: "sanitize_uri",
},
_ => return,
},
// airflow.providers.mysql
["airflow", "providers", "mysql", "datasets", "mysql", "sanitize_uri"] => {
Replacement::Name("airflow.providers.mysql.assets.mysql.sanitize_uri")
Replacement::AutoImport {
module: "airflow.providers.mysql.assets.mysql",
name: "sanitize_uri",
}
}
// airflow.providers.postgres
["airflow", "providers", "postgres", "datasets", "postgres", "sanitize_uri"] => {
Replacement::Name("airflow.providers.postgres.assets.postgres.sanitize_uri")
Replacement::AutoImport {
module: "airflow.providers.postgres.assets.postgres",
name: "sanitize_uri",
}
}
// airflow.providers.openlineage
@@ -859,16 +888,15 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
// airflow.providers.trino
["airflow", "providers", "trino", "datasets", "trino", "sanitize_uri"] => {
Replacement::Name("airflow.providers.trino.assets.trino.sanitize_uri")
Replacement::AutoImport {
module: "airflow.providers.trino.assets.trino",
name: "sanitize_uri",
}
}
_ => return,
};
if is_guarded_by_try_except(expr, &replacement, semantic) {
return;
}
let mut diagnostic = Diagnostic::new(
Airflow3Removal {
deprecated: qualified_name.to_string(),
@@ -876,8 +904,15 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
},
range,
);
if let Replacement::AutoImport { module, name } = replacement {
let semantic = checker.semantic();
if let Some((module, name)) = match &replacement {
Replacement::AutoImport { module, name } => Some((module, *name)),
Replacement::SourceModuleMoved { module, name } => Some((module, name.as_str())),
_ => None,
} {
if is_guarded_by_try_except(expr, module, name, semantic) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, name),
@@ -946,7 +981,7 @@ fn diagnostic_for_argument(
Airflow3Removal {
deprecated: deprecated.to_string(),
replacement: match replacement {
Some(name) => Replacement::Name(name),
Some(name) => Replacement::AttrName(name),
None => Replacement::None,
},
},

View File

@@ -1,6 +1,6 @@
use crate::importer::ImportRequest;
use crate::rules::airflow::helpers::ProviderReplacement;
use crate::rules::airflow::helpers::{is_guarded_by_try_except, ProviderReplacement};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{Expr, ExprAttribute};
@@ -279,13 +279,17 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
ranged.range(),
);
if let ProviderReplacement::AutoImport {
module,
name,
provider: _,
version: _,
} = replacement
{
let semantic = checker.semantic();
if let Some((module, name)) = match &replacement {
ProviderReplacement::AutoImport { module, name, .. } => Some((module, *name)),
ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => {
Some((module, name.as_str()))
}
_ => None,
} {
if is_guarded_by_try_except(expr, module, name, semantic) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, name),

View File

@@ -53,7 +53,8 @@ impl Violation for Airflow3SuggestedUpdate {
} = self;
match replacement {
Replacement::None
| Replacement::Name(_)
| Replacement::AttrName(_)
| Replacement::Message(_)
| Replacement::AutoImport { module: _, name: _ }
| Replacement::SourceModuleMoved { module: _, name: _ } => {
format!(
@@ -61,27 +62,21 @@ impl Violation for Airflow3SuggestedUpdate {
It still works in Airflow 3.0 but is expected to be removed in a future version."
)
}
Replacement::Message(message) => {
format!(
"`{deprecated}` is removed in Airflow 3.0; \
It still works in Airflow 3.0 but is expected to be removed in a future version.; \
{message}"
)
}
}
}
fn fix_title(&self) -> Option<String> {
let Airflow3SuggestedUpdate { replacement, .. } = self;
match replacement {
Replacement::Name(name) => Some(format!("Use `{name}` instead")),
Replacement::None => None,
Replacement::AttrName(name) => Some(format!("Use `{name}` instead")),
Replacement::Message(message) => Some((*message).to_string()),
Replacement::AutoImport { module, name } => {
Some(format!("Use `{module}.{name}` instead"))
}
Replacement::SourceModuleMoved { module, name } => {
Some(format!("Use `{module}.{name}` instead"))
}
_ => None,
}
}
}
@@ -126,7 +121,7 @@ fn diagnostic_for_argument(
Airflow3SuggestedUpdate {
deprecated: deprecated.to_string(),
replacement: match replacement {
Some(name) => Replacement::Name(name),
Some(name) => Replacement::AttrName(name),
None => Replacement::None,
},
},
@@ -242,6 +237,14 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
name: "attach".to_string(),
},
// airflow.models
["airflow", "models", rest @ ("Connection" | "Variable")] => {
Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: (*rest).to_string(),
}
}
// airflow.models.baseoperator
["airflow", "models", "baseoperator", rest] => match *rest {
"chain" | "chain_linear" | "cross_downstream" => Replacement::SourceModuleMoved {
@@ -275,10 +278,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
_ => return,
};
if is_guarded_by_try_except(expr, &replacement, semantic) {
return;
}
let mut diagnostic = Diagnostic::new(
Airflow3SuggestedUpdate {
deprecated: qualified_name.to_string(),
@@ -287,7 +286,15 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
range,
);
if let Replacement::AutoImport { module, name } = replacement {
let semantic = checker.semantic();
if let Some((module, name)) = match &replacement {
Replacement::AutoImport { module, name } => Some((module, *name)),
Replacement::SourceModuleMoved { module, name } => Some((module, name.as_str())),
_ => None,
} {
if is_guarded_by_try_except(expr, module, name, semantic) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, name),

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0
|
5 | name = "test_plugin"
6 | # --- Invalid extensions start
@@ -10,8 +10,9 @@ AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0; This
8 | sensors = [PluginSensorOperator]
9 | hooks = [PluginHook]
|
= help: This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0
|
6 | # --- Invalid extensions start
7 | operators = [PluginOperator]
@@ -20,8 +21,9 @@ AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0; This e
9 | hooks = [PluginHook]
10 | executors = [PluginExecutor]
|
= help: This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0
|
7 | operators = [PluginOperator]
8 | sensors = [PluginSensorOperator]
@@ -30,8 +32,9 @@ AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0; This ext
10 | executors = [PluginExecutor]
11 | # --- Invalid extensions end
|
= help: This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0
|
8 | sensors = [PluginSensorOperator]
9 | hooks = [PluginHook]
@@ -40,3 +43,4 @@ AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0; Thi
11 | # --- Invalid extensions end
12 | macros = [plugin_macro]
|
= help: This extension should just be imported as a regular python module.

View File

@@ -258,10 +258,11 @@ AIR301_args.py:90:16: AIR301 `filename_template` is removed in Airflow 3.0
92 | FabAuthManager(None)
|
AIR301_args.py:92:15: AIR301 `appbuilder` is removed in Airflow 3.0; The constructor takes no parameter now
AIR301_args.py:92:15: AIR301 `appbuilder` is removed in Airflow 3.0
|
90 | GCSTaskHandler(filename_template="/tmp/test")
91 |
92 | FabAuthManager(None)
| ^^^^^^ AIR301
|
= help: The constructor takes no parameter now

View File

@@ -1,448 +1,488 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR301_names.py:56:1: AIR301 `airflow.PY36` is removed in Airflow 3.0
AIR301_names.py:53:1: AIR301 `airflow.PY36` is removed in Airflow 3.0
|
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^ AIR301
57 |
58 | # airflow.api_connexion.security
54 |
55 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:56:7: AIR301 `airflow.PY37` is removed in Airflow 3.0
AIR301_names.py:53:7: AIR301 `airflow.PY37` is removed in Airflow 3.0
|
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^ AIR301
57 |
58 | # airflow.api_connexion.security
54 |
55 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:56:13: AIR301 `airflow.PY38` is removed in Airflow 3.0
AIR301_names.py:53:13: AIR301 `airflow.PY38` is removed in Airflow 3.0
|
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^ AIR301
57 |
58 | # airflow.api_connexion.security
54 |
55 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:56:19: AIR301 `airflow.PY39` is removed in Airflow 3.0
AIR301_names.py:53:19: AIR301 `airflow.PY39` is removed in Airflow 3.0
|
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^ AIR301
57 |
58 | # airflow.api_connexion.security
54 |
55 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:56:25: AIR301 `airflow.PY310` is removed in Airflow 3.0
AIR301_names.py:53:25: AIR301 `airflow.PY310` is removed in Airflow 3.0
|
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^ AIR301
57 |
58 | # airflow.api_connexion.security
54 |
55 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:56:32: AIR301 `airflow.PY311` is removed in Airflow 3.0
AIR301_names.py:53:32: AIR301 `airflow.PY311` is removed in Airflow 3.0
|
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^ AIR301
57 |
58 | # airflow.api_connexion.security
54 |
55 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:56:39: AIR301 `airflow.PY312` is removed in Airflow 3.0
AIR301_names.py:53:39: AIR301 `airflow.PY312` is removed in Airflow 3.0
|
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^ AIR301
57 |
58 | # airflow.api_connexion.security
54 |
55 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:59:1: AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0
AIR301_names.py:56:1: AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0
|
58 | # airflow.api_connexion.security
59 | requires_access
55 | # airflow.api_connexion.security
56 | requires_access
| ^^^^^^^^^^^^^^^ AIR301
|
= help: Use `airflow.api_connexion.security.requires_access_*` instead
AIR301_names.py:63:1: AIR301 `airflow.configuration.get` is removed in Airflow 3.0
AIR301_names.py:60:1: AIR301 `airflow.configuration.get` is removed in Airflow 3.0
|
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^ AIR301
|
= help: Use `airflow.configuration.conf.get` instead
AIR301_names.py:63:6: AIR301 `airflow.configuration.getboolean` is removed in Airflow 3.0
AIR301_names.py:60:6: AIR301 `airflow.configuration.getboolean` is removed in Airflow 3.0
|
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.getboolean` instead
AIR301_names.py:63:18: AIR301 `airflow.configuration.getfloat` is removed in Airflow 3.0
AIR301_names.py:60:18: AIR301 `airflow.configuration.getfloat` is removed in Airflow 3.0
|
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.getfloat` instead
AIR301_names.py:63:28: AIR301 `airflow.configuration.getint` is removed in Airflow 3.0
AIR301_names.py:60:28: AIR301 `airflow.configuration.getint` is removed in Airflow 3.0
|
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.getint` instead
AIR301_names.py:63:36: AIR301 `airflow.configuration.has_option` is removed in Airflow 3.0
AIR301_names.py:60:36: AIR301 `airflow.configuration.has_option` is removed in Airflow 3.0
|
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.has_option` instead
AIR301_names.py:63:48: AIR301 `airflow.configuration.remove_option` is removed in Airflow 3.0
AIR301_names.py:60:48: AIR301 `airflow.configuration.remove_option` is removed in Airflow 3.0
|
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.remove_option` instead
AIR301_names.py:63:63: AIR301 `airflow.configuration.as_dict` is removed in Airflow 3.0
AIR301_names.py:60:63: AIR301 `airflow.configuration.as_dict` is removed in Airflow 3.0
|
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.as_dict` instead
AIR301_names.py:63:72: AIR301 `airflow.configuration.set` is removed in Airflow 3.0
AIR301_names.py:60:72: AIR301 `airflow.configuration.set` is removed in Airflow 3.0
|
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^ AIR301
|
= help: Use `airflow.configuration.conf.set` instead
AIR301_names.py:67:1: AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0; The whole `airflow.contrib` module has been removed.
AIR301_names.py:64:1: AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0
|
66 | # airflow.contrib.*
67 | AWSAthenaHook()
63 | # airflow.contrib.*
64 | AWSAthenaHook()
| ^^^^^^^^^^^^^ AIR301
|
= help: The whole `airflow.contrib` module has been removed.
AIR301_names.py:71:1: AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
AIR301_names.py:68:1: AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
|
70 | # airflow.datasets
71 | DatasetAliasEvent()
67 | # airflow.datasets
68 | DatasetAliasEvent()
| ^^^^^^^^^^^^^^^^^ AIR301
|
AIR301_names.py:75:1: AIR301 `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0
AIR301_names.py:72:1: AIR301 `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0
|
74 | # airflow.hooks
75 | BaseHook()
71 | # airflow.hooks
72 | BaseHook()
| ^^^^^^^^ AIR301
|
= help: Use `airflow.hooks.base.BaseHook` instead
AIR301_names.py:79:1: AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0; The whole `airflow.subdag` module has been removed.
AIR301_names.py:76:1: AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
|
78 | # airflow.operators.subdag.*
79 | SubDagOperator()
75 | # airflow.operators.subdag.*
76 | SubDagOperator()
| ^^^^^^^^^^^^^^ AIR301
80 |
81 | # airflow.providers.mysql
|
= help: The whole `airflow.subdag` module has been removed.
AIR301_names.py:82:7: AIR301 `airflow.providers.mysql.datasets.mysql.sanitize_uri` is removed in Airflow 3.0
AIR301_names.py:85:1: AIR301 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0
|
81 | # airflow.providers.mysql
82 | mysql.sanitize_uri
| ^^^^^^^^^^^^ AIR301
83 |
84 | # airflow.providers.postgres
|
= help: Use `airflow.providers.mysql.assets.mysql.sanitize_uri` instead
AIR301_names.py:85:10: AIR301 `airflow.providers.postgres.datasets.postgres.sanitize_uri` is removed in Airflow 3.0
|
84 | # airflow.providers.postgres
85 | postgres.sanitize_uri
| ^^^^^^^^^^^^ AIR301
86 |
87 | # airflow.providers.trino
|
= help: Use `airflow.providers.postgres.assets.postgres.sanitize_uri` instead
AIR301_names.py:88:7: AIR301 `airflow.providers.trino.datasets.trino.sanitize_uri` is removed in Airflow 3.0
|
87 | # airflow.providers.trino
88 | trino.sanitize_uri
| ^^^^^^^^^^^^ AIR301
89 |
90 | # airflow.secrets
|
= help: Use `airflow.providers.trino.assets.trino.sanitize_uri` instead
AIR301_names.py:96:1: AIR301 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0
|
95 | # airflow.sensors.base_sensor_operator
96 | BaseSensorOperator()
84 | # airflow.sensors.base_sensor_operator
85 | BaseSensorOperator()
| ^^^^^^^^^^^^^^^^^^ AIR301
|
= help: Use `airflow.sdk.bases.sensor.BaseSensorOperator` instead
AIR301_names.py:100:1: AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
AIR301_names.py:89:1: AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
|
88 | # airflow.triggers.external_task
89 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^ AIR301
90 |
91 | # airflow.utils.date
|
AIR301_names.py:92:7: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
91 | # airflow.utils.date
92 | dates.date_range
| ^^^^^^^^^^ AIR301
93 | dates.days_ago
|
AIR301_names.py:93:7: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
|
91 | # airflow.utils.date
92 | dates.date_range
93 | dates.days_ago
| ^^^^^^^^ AIR301
94 |
95 | date_range
|
= help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301_names.py:95:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
93 | dates.days_ago
94 |
95 | date_range
| ^^^^^^^^^^ AIR301
96 | days_ago
97 | infer_time_unit
|
AIR301_names.py:96:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
|
95 | date_range
96 | days_ago
| ^^^^^^^^ AIR301
97 | infer_time_unit
98 | parse_execution_date
|
= help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301_names.py:97:1: AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
|
95 | date_range
96 | days_ago
97 | infer_time_unit
| ^^^^^^^^^^^^^^^ AIR301
98 | parse_execution_date
99 | round_time
|
AIR301_names.py:98:1: AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
|
99 | # airflow.triggers.external_task
100 | TaskStateTrigger()
96 | days_ago
97 | infer_time_unit
98 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^ AIR301
99 | round_time
100 | scale_time_units
|
AIR301_names.py:99:1: AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
|
97 | infer_time_unit
98 | parse_execution_date
99 | round_time
| ^^^^^^^^^^ AIR301
100 | scale_time_units
|
AIR301_names.py:100:1: AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
|
98 | parse_execution_date
99 | round_time
100 | scale_time_units
| ^^^^^^^^^^^^^^^^ AIR301
101 |
102 | # airflow.utils.date
102 | # This one was not deprecated.
|
AIR301_names.py:103:7: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
AIR301_names.py:107:1: AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
|
102 | # airflow.utils.date
103 | dates.date_range
| ^^^^^^^^^^ AIR301
104 | dates.days_ago
|
AIR301_names.py:104:7: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
|
102 | # airflow.utils.date
103 | dates.date_range
104 | dates.days_ago
| ^^^^^^^^ AIR301
105 |
106 | date_range
|
= help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301_names.py:106:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
104 | dates.days_ago
105 |
106 | date_range
106 | # airflow.utils.dag_cycle_tester
107 | test_cycle
| ^^^^^^^^^^ AIR301
107 | days_ago
108 | infer_time_unit
|
AIR301_names.py:107:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
AIR301_names.py:111:1: AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
|
106 | date_range
107 | days_ago
| ^^^^^^^^ AIR301
108 | infer_time_unit
109 | parse_execution_date
|
= help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301_names.py:108:1: AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
|
106 | date_range
107 | days_ago
108 | infer_time_unit
| ^^^^^^^^^^^^^^^ AIR301
109 | parse_execution_date
110 | round_time
|
AIR301_names.py:109:1: AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
|
107 | days_ago
108 | infer_time_unit
109 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^ AIR301
110 | round_time
111 | scale_time_units
|
AIR301_names.py:110:1: AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
|
108 | infer_time_unit
109 | parse_execution_date
110 | round_time
| ^^^^^^^^^^ AIR301
111 | scale_time_units
|
AIR301_names.py:111:1: AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
|
109 | parse_execution_date
110 | round_time
111 | scale_time_units
| ^^^^^^^^^^^^^^^^ AIR301
110 | # airflow.utils.db
111 | create_session
| ^^^^^^^^^^^^^^ AIR301
112 |
113 | # This one was not deprecated.
113 | # airflow.utils.decorators
|
AIR301_names.py:118:1: AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
AIR301_names.py:114:1: AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0
|
117 | # airflow.utils.dag_cycle_tester
118 | test_cycle
| ^^^^^^^^^^ AIR301
|
AIR301_names.py:122:1: AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
|
121 | # airflow.utils.db
122 | create_session
113 | # airflow.utils.decorators
114 | apply_defaults
| ^^^^^^^^^^^^^^ AIR301
123 |
124 | # airflow.utils.decorators
115 |
116 | # airflow.utils.file
|
= help: `apply_defaults` is now unconditionally done and can be safely removed.
AIR301_names.py:125:1: AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0; `apply_defaults` is now unconditionally done and can be safely removed.
AIR301_names.py:117:1: AIR301 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0
|
124 | # airflow.utils.decorators
125 | apply_defaults
| ^^^^^^^^^^^^^^ AIR301
126 |
127 | # airflow.utils.file
|
AIR301_names.py:128:1: AIR301 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0
|
127 | # airflow.utils.file
128 | TemporaryDirectory()
116 | # airflow.utils.file
117 | TemporaryDirectory()
| ^^^^^^^^^^^^^^^^^^ AIR301
129 | mkdirs
118 | mkdirs
|
= help: Use `tempfile.TemporaryDirectory` instead
AIR301_names.py:129:1: AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
AIR301_names.py:118:1: AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
|
127 | # airflow.utils.file
128 | TemporaryDirectory()
129 | mkdirs
116 | # airflow.utils.file
117 | TemporaryDirectory()
118 | mkdirs
| ^^^^^^ AIR301
130 |
131 | # airflow.utils.helpers
119 |
120 | # airflow.utils.helpers
|
= help: Use `pathlib.Path({path}).mkdir` instead
AIR301_names.py:132:1: AIR301 `airflow.utils.helpers.chain` is removed in Airflow 3.0
AIR301_names.py:121:1: AIR301 [*] `airflow.utils.helpers.chain` is removed in Airflow 3.0
|
131 | # airflow.utils.helpers
132 | helper_chain
120 | # airflow.utils.helpers
121 | helper_chain
| ^^^^^^^^^^^^ AIR301
133 | helper_cross_downstream
122 | helper_cross_downstream
|
= help: Use `airflow.sdk.chain` instead
AIR301_names.py:133:1: AIR301 `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0
Safe fix
48 48 | from airflow.utils.trigger_rule import TriggerRule
49 49 | from airflow.www.auth import has_access
50 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
51 |+from airflow.sdk import chain
51 52 |
52 53 | # airflow root
53 54 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
--------------------------------------------------------------------------------
118 119 | mkdirs
119 120 |
120 121 | # airflow.utils.helpers
121 |-helper_chain
122 |+chain
122 123 | helper_cross_downstream
123 124 |
124 125 | # airflow.utils.log
AIR301_names.py:122:1: AIR301 [*] `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0
|
131 | # airflow.utils.helpers
132 | helper_chain
133 | helper_cross_downstream
120 | # airflow.utils.helpers
121 | helper_chain
122 | helper_cross_downstream
| ^^^^^^^^^^^^^^^^^^^^^^^ AIR301
134 |
135 | # airflow.utils.log
123 |
124 | # airflow.utils.log
|
= help: Use `airflow.sdk.cross_downstream` instead
AIR301_names.py:136:1: AIR301 `airflow.utils.log.secrets_masker` is removed in Airflow 3.0
Safe fix
48 48 | from airflow.utils.trigger_rule import TriggerRule
49 49 | from airflow.www.auth import has_access
50 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
51 |+from airflow.sdk import cross_downstream
51 52 |
52 53 | # airflow root
53 54 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
--------------------------------------------------------------------------------
119 120 |
120 121 | # airflow.utils.helpers
121 122 | helper_chain
122 |-helper_cross_downstream
123 |+cross_downstream
123 124 |
124 125 | # airflow.utils.log
125 126 | secrets_masker
AIR301_names.py:125:1: AIR301 `airflow.utils.log.secrets_masker` is removed in Airflow 3.0
|
135 | # airflow.utils.log
136 | secrets_masker
124 | # airflow.utils.log
125 | secrets_masker
| ^^^^^^^^^^^^^^ AIR301
137 |
138 | # airflow.utils.state
126 |
127 | # airflow.utils.state
|
= help: Use `airflow.sdk.execution_time.secrets_masker` instead
AIR301_names.py:139:1: AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
AIR301_names.py:128:1: AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
|
138 | # airflow.utils.state
139 | SHUTDOWN
127 | # airflow.utils.state
128 | SHUTDOWN
| ^^^^^^^^ AIR301
140 | terminating_states
129 | terminating_states
|
AIR301_names.py:140:1: AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
AIR301_names.py:129:1: AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
|
138 | # airflow.utils.state
139 | SHUTDOWN
140 | terminating_states
127 | # airflow.utils.state
128 | SHUTDOWN
129 | terminating_states
| ^^^^^^^^^^^^^^^^^^ AIR301
141 |
142 | # airflow.utils.trigger_rule
130 |
131 | # airflow.utils.trigger_rule
|
AIR301_names.py:143:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
AIR301_names.py:132:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
|
142 | # airflow.utils.trigger_rule
143 | TriggerRule.DUMMY
131 | # airflow.utils.trigger_rule
132 | TriggerRule.DUMMY
| ^^^^^ AIR301
144 | TriggerRule.NONE_FAILED_OR_SKIPPED
133 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
AIR301_names.py:144:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
AIR301_names.py:133:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
|
142 | # airflow.utils.trigger_rule
143 | TriggerRule.DUMMY
144 | TriggerRule.NONE_FAILED_OR_SKIPPED
131 | # airflow.utils.trigger_rule
132 | TriggerRule.DUMMY
133 | TriggerRule.NONE_FAILED_OR_SKIPPED
| ^^^^^^^^^^^^^^^^^^^^^^ AIR301
|
AIR301_names.py:148:1: AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
AIR301_names.py:137:1: AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
|
147 | # airflow.www.auth
148 | has_access
136 | # airflow.www.auth
137 | has_access
| ^^^^^^^^^^ AIR301
149 |
150 | # airflow.www.utils
138 |
139 | # airflow.www.utils
|
= help: Use `airflow.www.auth.has_access_*` instead
AIR301_names.py:151:1: AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
AIR301_names.py:140:1: AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
|
150 | # airflow.www.utils
151 | get_sensitive_variables_fields
139 | # airflow.www.utils
140 | get_sensitive_variables_fields
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301
152 | should_hide_value_for_key
141 | should_hide_value_for_key
|
= help: Use `airflow.utils.log.secrets_masker.get_sensitive_variables_fields` instead
AIR301_names.py:152:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
AIR301_names.py:141:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
|
150 | # airflow.www.utils
151 | get_sensitive_variables_fields
152 | should_hide_value_for_key
139 | # airflow.www.utils
140 | get_sensitive_variables_fields
141 | should_hide_value_for_key
| ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301
153 |
154 | # airflow.operators.python
142 |
143 | # airflow.operators.python
|
= help: Use `airflow.utils.log.secrets_masker.should_hide_value_for_key` instead
AIR301_names.py:157:1: AIR301 `airflow.operators.python.get_current_context` is removed in Airflow 3.0
AIR301_names.py:146:1: AIR301 `airflow.operators.python.get_current_context` is removed in Airflow 3.0
|
155 | from airflow.operators.python import get_current_context
156 |
157 | get_current_context()
144 | from airflow.operators.python import get_current_context
145 |
146 | get_current_context()
| ^^^^^^^^^^^^^^^^^^^ AIR301
147 |
148 | # airflow.providers.mysql
|
= help: Use `airflow.sdk.get_current_context` instead
AIR301_names.py:151:1: AIR301 `airflow.providers.mysql.datasets.mysql.sanitize_uri` is removed in Airflow 3.0
|
149 | from airflow.providers.mysql.datasets.mysql import sanitize_uri
150 |
151 | sanitize_uri
| ^^^^^^^^^^^^ AIR301
152 |
153 | # airflow.providers.postgres
|
= help: Use `airflow.providers.mysql.assets.mysql.sanitize_uri` instead
AIR301_names.py:156:1: AIR301 `airflow.providers.postgres.datasets.postgres.sanitize_uri` is removed in Airflow 3.0
|
154 | from airflow.providers.postgres.datasets.postgres import sanitize_uri
155 |
156 | sanitize_uri
| ^^^^^^^^^^^^ AIR301
157 |
158 | # airflow.providers.trino
|
= help: Use `airflow.providers.postgres.assets.postgres.sanitize_uri` instead
AIR301_names.py:161:1: AIR301 `airflow.providers.trino.datasets.trino.sanitize_uri` is removed in Airflow 3.0
|
159 | from airflow.providers.trino.datasets.trino import sanitize_uri
160 |
161 | sanitize_uri
| ^^^^^^^^^^^^ AIR301
|
= help: Use `airflow.providers.trino.assets.trino.sanitize_uri` instead

View File

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

View File

@@ -1,337 +1,394 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR311_names.py:23:1: AIR311 [*] `airflow.Dataset` 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:27:1: AIR311 [*] `airflow.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
22 | # airflow
23 | DatasetFromRoot()
26 | # airflow
27 | DatasetFromRoot()
| ^^^^^^^^^^^^^^^ AIR311
24 |
25 | # airflow.datasets
28 |
29 | # airflow.datasets
|
= help: Use `airflow.sdk.Asset` instead
Safe fix
18 18 | from airflow.models.dag import DAG as DAGFromDag
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
21 |+from airflow.sdk import Asset
21 22 |
22 23 | # airflow
23 |-DatasetFromRoot()
24 |+Asset()
24 25 |
25 26 | # airflow.datasets
26 27 | Dataset()
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.sdk import Asset
25 26 |
26 27 | # airflow
27 |-DatasetFromRoot()
28 |+Asset()
28 29 |
29 30 | # airflow.datasets
30 31 | Dataset()
AIR311_names.py:26:1: AIR311 [*] `airflow.datasets.Dataset` 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:30:1: AIR311 [*] `airflow.datasets.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
25 | # airflow.datasets
26 | Dataset()
29 | # airflow.datasets
30 | Dataset()
| ^^^^^^^ AIR311
27 | DatasetAlias()
28 | DatasetAll()
31 | DatasetAlias()
32 | DatasetAll()
|
= help: Use `airflow.sdk.Asset` instead
Safe fix
18 18 | from airflow.models.dag import DAG as DAGFromDag
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
21 |+from airflow.sdk import Asset
21 22 |
22 23 | # airflow
23 24 | DatasetFromRoot()
24 25 |
25 26 | # airflow.datasets
26 |-Dataset()
27 |+Asset()
27 28 | DatasetAlias()
28 29 | DatasetAll()
29 30 | DatasetAny()
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.sdk import Asset
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
28 29 |
29 30 | # airflow.datasets
30 |-Dataset()
31 |+Asset()
31 32 | DatasetAlias()
32 33 | DatasetAll()
33 34 | DatasetAny()
AIR311_names.py:27:1: AIR311 [*] `airflow.datasets.DatasetAlias` 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:31:1: AIR311 [*] `airflow.datasets.DatasetAlias` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
25 | # airflow.datasets
26 | Dataset()
27 | DatasetAlias()
29 | # airflow.datasets
30 | Dataset()
31 | DatasetAlias()
| ^^^^^^^^^^^^ AIR311
28 | DatasetAll()
29 | DatasetAny()
32 | DatasetAll()
33 | DatasetAny()
|
= help: Use `airflow.sdk.AssetAlias` instead
Safe fix
18 18 | from airflow.models.dag import DAG as DAGFromDag
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
21 |+from airflow.sdk import AssetAlias
21 22 |
22 23 | # airflow
23 24 | DatasetFromRoot()
24 25 |
25 26 | # airflow.datasets
26 27 | Dataset()
27 |-DatasetAlias()
28 |+AssetAlias()
28 29 | DatasetAll()
29 30 | DatasetAny()
30 31 | Metadata()
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.sdk import AssetAlias
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
28 29 |
29 30 | # airflow.datasets
30 31 | Dataset()
31 |-DatasetAlias()
32 |+AssetAlias()
32 33 | DatasetAll()
33 34 | DatasetAny()
34 35 | Metadata()
AIR311_names.py:28:1: AIR311 [*] `airflow.datasets.DatasetAll` 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:32:1: AIR311 [*] `airflow.datasets.DatasetAll` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
26 | Dataset()
27 | DatasetAlias()
28 | DatasetAll()
30 | Dataset()
31 | DatasetAlias()
32 | DatasetAll()
| ^^^^^^^^^^ AIR311
29 | DatasetAny()
30 | Metadata()
33 | DatasetAny()
34 | Metadata()
|
= help: Use `airflow.sdk.AssetAll` instead
Safe fix
18 18 | from airflow.models.dag import DAG as DAGFromDag
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
21 |+from airflow.sdk import AssetAll
21 22 |
22 23 | # airflow
23 24 | DatasetFromRoot()
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.sdk import AssetAll
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
--------------------------------------------------------------------------------
25 26 | # airflow.datasets
26 27 | Dataset()
27 28 | DatasetAlias()
28 |-DatasetAll()
29 |+AssetAll()
29 30 | DatasetAny()
30 31 | Metadata()
31 32 | expand_alias_to_datasets()
29 30 | # airflow.datasets
30 31 | Dataset()
31 32 | DatasetAlias()
32 |-DatasetAll()
33 |+AssetAll()
33 34 | DatasetAny()
34 35 | Metadata()
35 36 | expand_alias_to_datasets()
AIR311_names.py:29:1: AIR311 [*] `airflow.datasets.DatasetAny` 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:33:1: AIR311 [*] `airflow.datasets.DatasetAny` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
27 | DatasetAlias()
28 | DatasetAll()
29 | DatasetAny()
31 | DatasetAlias()
32 | DatasetAll()
33 | DatasetAny()
| ^^^^^^^^^^ AIR311
30 | Metadata()
31 | expand_alias_to_datasets()
34 | Metadata()
35 | expand_alias_to_datasets()
|
= help: Use `airflow.sdk.AssetAny` instead
Safe fix
18 18 | from airflow.models.dag import DAG as DAGFromDag
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
21 |+from airflow.sdk import AssetAny
21 22 |
22 23 | # airflow
23 24 | DatasetFromRoot()
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.sdk import AssetAny
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
--------------------------------------------------------------------------------
26 27 | Dataset()
27 28 | DatasetAlias()
28 29 | DatasetAll()
29 |-DatasetAny()
30 |+AssetAny()
30 31 | Metadata()
31 32 | expand_alias_to_datasets()
32 33 |
30 31 | Dataset()
31 32 | DatasetAlias()
32 33 | DatasetAll()
33 |-DatasetAny()
34 |+AssetAny()
34 35 | Metadata()
35 36 | expand_alias_to_datasets()
36 37 |
AIR311_names.py:30:1: AIR311 `airflow.datasets.metadata.Metadata` 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:34:1: AIR311 `airflow.datasets.metadata.Metadata` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
28 | DatasetAll()
29 | DatasetAny()
30 | Metadata()
32 | DatasetAll()
33 | DatasetAny()
34 | Metadata()
| ^^^^^^^^ AIR311
31 | expand_alias_to_datasets()
35 | expand_alias_to_datasets()
|
= help: Use `airflow.sdk.Metadata` instead
AIR311_names.py:31:1: AIR311 [*] `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
AIR311_names.py:35:1: AIR311 [*] `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
29 | DatasetAny()
30 | Metadata()
31 | expand_alias_to_datasets()
33 | DatasetAny()
34 | Metadata()
35 | expand_alias_to_datasets()
| ^^^^^^^^^^^^^^^^^^^^^^^^ AIR311
32 |
33 | # airflow.decorators
36 |
37 | # airflow.decorators
|
= help: Use `airflow.sdk.expand_alias_to_assets` instead
Safe fix
18 18 | from airflow.models.dag import DAG as DAGFromDag
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
21 |+from airflow.sdk import expand_alias_to_assets
21 22 |
22 23 | # airflow
23 24 | DatasetFromRoot()
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.sdk import expand_alias_to_assets
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
--------------------------------------------------------------------------------
28 29 | DatasetAll()
29 30 | DatasetAny()
30 31 | Metadata()
31 |-expand_alias_to_datasets()
32 |+expand_alias_to_assets()
32 33 |
33 34 | # airflow.decorators
34 35 | dag()
32 33 | DatasetAll()
33 34 | DatasetAny()
34 35 | Metadata()
35 |-expand_alias_to_datasets()
36 |+expand_alias_to_assets()
36 37 |
37 38 | # airflow.decorators
38 39 | dag()
AIR311_names.py:34:1: AIR311 `airflow.decorators.dag` 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:38:1: AIR311 `airflow.decorators.dag` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
33 | # airflow.decorators
34 | dag()
37 | # airflow.decorators
38 | dag()
| ^^^ AIR311
35 | task()
36 | task_group()
39 | task()
40 | task_group()
|
= help: Use `airflow.sdk.dag` instead
AIR311_names.py:35:1: AIR311 `airflow.decorators.task` 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:39:1: AIR311 `airflow.decorators.task` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
33 | # airflow.decorators
34 | dag()
35 | task()
37 | # airflow.decorators
38 | dag()
39 | task()
| ^^^^ AIR311
36 | task_group()
37 | setup()
40 | task_group()
41 | setup()
|
= help: Use `airflow.sdk.task` instead
AIR311_names.py:36:1: AIR311 `airflow.decorators.task_group` 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:40:1: AIR311 `airflow.decorators.task_group` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
34 | dag()
35 | task()
36 | task_group()
38 | dag()
39 | task()
40 | task_group()
| ^^^^^^^^^^ AIR311
37 | setup()
38 | teardown()
41 | setup()
42 | teardown()
|
= help: Use `airflow.sdk.task_group` instead
AIR311_names.py:37:1: AIR311 `airflow.decorators.setup` 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:41:1: AIR311 `airflow.decorators.setup` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
35 | task()
36 | task_group()
37 | setup()
39 | task()
40 | task_group()
41 | setup()
| ^^^^^ AIR311
38 | teardown()
42 | teardown()
|
= help: Use `airflow.sdk.setup` instead
AIR311_names.py:38:1: AIR311 `airflow.decorators.teardown` 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:42:1: AIR311 `airflow.decorators.teardown` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
36 | task_group()
37 | setup()
38 | teardown()
40 | task_group()
41 | setup()
42 | teardown()
| ^^^^^^^^ AIR311
39 |
40 | # airflow.io
43 |
44 | # airflow.io
|
= help: Use `airflow.sdk.teardown` instead
AIR311_names.py:41:1: AIR311 `airflow.io.path.ObjectStoragePath` 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:45:1: AIR311 `airflow.io.path.ObjectStoragePath` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
40 | # airflow.io
41 | ObjectStoragePath()
44 | # airflow.io
45 | ObjectStoragePath()
| ^^^^^^^^^^^^^^^^^ AIR311
42 | attach()
46 | attach()
|
= help: Use `airflow.sdk.ObjectStoragePath` instead
AIR311_names.py:42:1: 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_names.py:46:1: 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.
|
40 | # airflow.io
41 | ObjectStoragePath()
42 | attach()
44 | # airflow.io
45 | ObjectStoragePath()
46 | attach()
| ^^^^^^ AIR311
43 |
44 | # airflow.models
47 |
48 | # airflow.models
|
= help: Use `airflow.sdk.io.attach` instead
AIR311_names.py:45:1: AIR311 `airflow.models.DAG` 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:49:1: AIR311 `airflow.models.Connection` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
44 | # airflow.models
45 | DAGFromModel()
48 | # airflow.models
49 | Connection()
| ^^^^^^^^^^ AIR311
50 | DAGFromModel()
51 | Variable()
|
= help: Use `airflow.sdk.Connection` instead
AIR311_names.py:50:1: AIR311 [*] `airflow.models.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
48 | # airflow.models
49 | Connection()
50 | DAGFromModel()
| ^^^^^^^^^^^^ AIR311
46 |
47 | # airflow.models.baseoperator
51 | Variable()
|
= help: Use `airflow.sdk.DAG` instead
AIR311_names.py:48:1: AIR311 `airflow.models.baseoperator.chain` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
Safe fix
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.sdk import DAG
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
--------------------------------------------------------------------------------
47 48 |
48 49 | # airflow.models
49 50 | Connection()
50 |-DAGFromModel()
51 |+DAG()
51 52 | Variable()
52 53 |
53 54 | # airflow.models.baseoperator
AIR311_names.py:51:1: AIR311 `airflow.models.Variable` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
47 | # airflow.models.baseoperator
48 | chain()
49 | Connection()
50 | DAGFromModel()
51 | Variable()
| ^^^^^^^^ AIR311
52 |
53 | # airflow.models.baseoperator
|
= help: Use `airflow.sdk.Variable` instead
AIR311_names.py:54:1: AIR311 `airflow.models.baseoperator.chain` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
53 | # airflow.models.baseoperator
54 | chain()
| ^^^^^ AIR311
49 | chain_linear()
50 | cross_downstream()
55 | chain_linear()
56 | cross_downstream()
|
= help: Use `airflow.sdk.chain` instead
AIR311_names.py:49:1: AIR311 `airflow.models.baseoperator.chain_linear` 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:55:1: AIR311 `airflow.models.baseoperator.chain_linear` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
47 | # airflow.models.baseoperator
48 | chain()
49 | chain_linear()
53 | # airflow.models.baseoperator
54 | chain()
55 | chain_linear()
| ^^^^^^^^^^^^ AIR311
50 | cross_downstream()
56 | cross_downstream()
|
= help: Use `airflow.sdk.chain_linear` instead
AIR311_names.py:50:1: AIR311 `airflow.models.baseoperator.cross_downstream` 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:56:1: AIR311 `airflow.models.baseoperator.cross_downstream` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
48 | chain()
49 | chain_linear()
50 | cross_downstream()
54 | chain()
55 | chain_linear()
56 | cross_downstream()
| ^^^^^^^^^^^^^^^^ AIR311
51 |
52 | # airflow.models.baseoperatolinker
57 |
58 | # airflow.models.baseoperatolinker
|
= help: Use `airflow.sdk.cross_downstream` instead
AIR311_names.py:56:1: AIR311 `airflow.models.dag.DAG` 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:62:1: AIR311 [*] `airflow.models.dag.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
55 | # airflow.models.dag
56 | DAGFromDag()
61 | # airflow.models.dag
62 | DAGFromDag()
| ^^^^^^^^^^ AIR311
57 | # airflow.timetables.datasets
58 | DatasetOrTimeSchedule()
63 | # airflow.timetables.datasets
64 | DatasetOrTimeSchedule()
|
= help: Use `airflow.sdk.DAG` instead
AIR311_names.py:58:1: AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
Safe fix
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.sdk import DAG
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
--------------------------------------------------------------------------------
59 60 | BaseOperatorLink()
60 61 |
61 62 | # airflow.models.dag
62 |-DAGFromDag()
63 |+DAG()
63 64 | # airflow.timetables.datasets
64 65 | DatasetOrTimeSchedule()
65 66 |
AIR311_names.py:64:1: AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
56 | DAGFromDag()
57 | # airflow.timetables.datasets
58 | DatasetOrTimeSchedule()
62 | DAGFromDag()
63 | # airflow.timetables.datasets
64 | DatasetOrTimeSchedule()
| ^^^^^^^^^^^^^^^^^^^^^ AIR311
59 |
60 | # airflow.utils.dag_parsing_context
65 |
66 | # airflow.utils.dag_parsing_context
|
= help: Use `airflow.timetables.assets.AssetOrTimeSchedule` instead
Safe fix
18 18 | from airflow.models.dag import DAG as DAGFromDag
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
21 |+from airflow.timetables.assets import AssetOrTimeSchedule
21 22 |
22 23 | # airflow
23 24 | DatasetFromRoot()
22 22 | from airflow.models.dag import DAG as DAGFromDag
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
25 |+from airflow.timetables.assets import AssetOrTimeSchedule
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
--------------------------------------------------------------------------------
55 56 | # airflow.models.dag
56 57 | DAGFromDag()
57 58 | # airflow.timetables.datasets
58 |-DatasetOrTimeSchedule()
59 |+AssetOrTimeSchedule()
59 60 |
60 61 | # airflow.utils.dag_parsing_context
61 62 | get_parsing_context()
61 62 | # airflow.models.dag
62 63 | DAGFromDag()
63 64 | # airflow.timetables.datasets
64 |-DatasetOrTimeSchedule()
65 |+AssetOrTimeSchedule()
65 66 |
66 67 | # airflow.utils.dag_parsing_context
67 68 | get_parsing_context()
AIR311_names.py:61:1: AIR311 `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
AIR311_names.py:67:1: AIR311 `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
60 | # airflow.utils.dag_parsing_context
61 | get_parsing_context()
66 | # airflow.utils.dag_parsing_context
67 | get_parsing_context()
| ^^^^^^^^^^^^^^^^^^^ AIR311
|
= help: Use `airflow.sdk.get_parsing_context` instead

View File

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

View File

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

View File

@@ -100,7 +100,15 @@ pub(crate) fn hardcoded_sql_expression(checker: &Checker, expr: &Expr) {
}
// f"select * from table where val = {val}"
Expr::FString(f_string) => concatenated_f_string(f_string, checker.locator()),
Expr::FString(f_string)
if f_string
.value
.f_strings()
.any(|fs| fs.elements.iter().any(ast::FStringElement::is_expression)) =>
{
concatenated_f_string(f_string, checker.locator())
}
_ => return,
};

View File

@@ -601,4 +601,6 @@ S608.py:164:11: S608 Possible SQL injection vector through string-based query co
167 | | FROM ({user_input}) raw
168 | | """
| |___^ S608
169 |
170 | # https://github.com/astral-sh/ruff/issues/17967
|

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
use crate::{checkers::ast::Checker, fix::edits::add_argument};
@@ -60,10 +60,18 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) {
return;
}
// When prefixes are supplied, stacklevel is implicitly overridden to be `max(2, stacklevel)`.
//
// Signature as of Python 3.13 (https://docs.python.org/3/library/warnings.html#warnings.warn)
// ```text
// 0 1 2 3 4
// warnings.warn(message, category=None, stacklevel=1, source=None, *, skip_file_prefixes=())
// ```
if call
.arguments
.find_argument_value("stacklevel", 2)
.is_some()
|| is_skip_file_prefixes_param_set(&call.arguments)
|| call
.arguments
.args
@@ -90,3 +98,14 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) {
checker.report_diagnostic(diagnostic);
}
/// Returns `true` if `skip_file_prefixes` is set to its non-default value.
/// The default value of `skip_file_prefixes` is an empty tuple.
fn is_skip_file_prefixes_param_set(arguments: &ast::Arguments) -> bool {
arguments
.find_keyword("skip_file_prefixes")
.is_some_and(|keyword| match &keyword.value {
Expr::Tuple(tuple) => !tuple.elts.is_empty(),
_ => true,
})
}

View File

@@ -61,3 +61,26 @@ B028.py:22:1: B028 [*] No explicit `stacklevel` keyword argument found
26 |- source = None # no trailing comma
26 |+ source = None, stacklevel=2 # no trailing comma
27 27 | )
28 28 |
29 29 | # https://github.com/astral-sh/ruff/issues/18011
B028.py:32:1: B028 [*] No explicit `stacklevel` keyword argument found
|
30 | warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),))
31 | # trigger diagnostic if `skip_file_prefixes` is present and set to the default value
32 | warnings.warn("test", skip_file_prefixes=())
| ^^^^^^^^^^^^^ B028
33 |
34 | _my_prefixes = ("this","that")
|
= help: Set `stacklevel=2`
Unsafe fix
29 29 | # https://github.com/astral-sh/ruff/issues/18011
30 30 | warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),))
31 31 | # trigger diagnostic if `skip_file_prefixes` is present and set to the default value
32 |-warnings.warn("test", skip_file_prefixes=())
32 |+warnings.warn("test", skip_file_prefixes=(), stacklevel=2)
33 33 |
34 34 | _my_prefixes = ("this","that")
35 35 | warnings.warn("test", skip_file_prefixes = _my_prefixes)

View File

@@ -1,7 +1,7 @@
use itertools::Itertools;
use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Expr};
@@ -19,6 +19,7 @@ use crate::fix::edits::{remove_argument, Parentheses};
/// arguments directly.
///
/// ## Example
///
/// ```python
/// def foo(bar):
/// return bar + 1
@@ -28,6 +29,7 @@ use crate::fix::edits::{remove_argument, Parentheses};
/// ```
///
/// Use instead:
///
/// ```python
/// def foo(bar):
/// return bar + 1
@@ -36,6 +38,26 @@ use crate::fix::edits::{remove_argument, Parentheses};
/// print(foo(bar=2)) # prints 3
/// ```
///
/// ## Fix safety
///
/// This rule's fix is marked as unsafe for dictionaries with comments interleaved between
/// the items, as comments may be removed.
///
/// For example, the fix would be marked as unsafe in the following case:
///
/// ```python
/// foo(
/// **{
/// # comment
/// "x": 1.0,
/// # comment
/// "y": 2.0,
/// }
/// )
/// ```
///
/// as this is converted to `foo(x=1.0, y=2.0)` without any of the comments.
///
/// ## References
/// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays)
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
@@ -113,7 +135,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
.iter()
.all(|kwarg| !duplicate_keywords.contains(kwarg))
{
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
let edit = Edit::range_replacement(
kwargs
.iter()
.zip(dict.iter_values())
@@ -134,7 +156,15 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
})
.join(", "),
keyword.range(),
)));
);
diagnostic.set_fix(Fix::applicable_edit(
edit,
if checker.comment_ranges().intersects(dict.range()) {
Applicability::Unsafe
} else {
Applicability::Safe
},
));
}
}
}

View File

@@ -208,6 +208,8 @@ PIE804.py:29:12: PIE804 [*] Unnecessary `dict` kwargs
29 |-abc(foo=1, **{'bar': (bar := 1)}) # PIE804
29 |+abc(foo=1, bar=(bar := 1)) # PIE804
30 30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
31 31 |
32 32 | # https://github.com/astral-sh/ruff/issues/18036
PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
|
@@ -215,6 +217,8 @@ PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^ PIE804
31 |
32 | # https://github.com/astral-sh/ruff/issues/18036
|
= help: Remove unnecessary kwargs
@@ -224,3 +228,34 @@ PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
30 |-abc(foo=1, **{'bar': (yield 1)}) # PIE804
30 |+abc(foo=1, bar=(yield 1)) # PIE804
31 31 |
32 32 | # https://github.com/astral-sh/ruff/issues/18036
33 33 | # The autofix for this is unsafe due to the comments inside the dictionary.
PIE804.py:35:5: PIE804 [*] Unnecessary `dict` kwargs
|
33 | # The autofix for this is unsafe due to the comments inside the dictionary.
34 | foo(
35 | / **{
36 | | # Comment 1
37 | | "x": 1.0,
38 | | # Comment 2
39 | | "y": 2.0,
40 | | }
| |_____^ PIE804
41 | )
|
= help: Remove unnecessary kwargs
Unsafe fix
32 32 | # https://github.com/astral-sh/ruff/issues/18036
33 33 | # The autofix for this is unsafe due to the comments inside the dictionary.
34 34 | foo(
35 |- **{
36 |- # Comment 1
37 |- "x": 1.0,
38 |- # Comment 2
39 |- "y": 2.0,
40 |- }
35 |+ x=1.0, y=2.0
41 36 | )

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::name::UnqualifiedName;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
@@ -11,6 +12,7 @@ use ruff_python_semantic::SemanticModel;
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
use ruff_text_size::{TextLen, TextRange};
use rustc_hash::FxHashSet;
use crate::checkers::ast::Checker;
use crate::fix::edits;
@@ -807,10 +809,51 @@ fn check_fixture_returns(checker: &Checker, name: &str, body: &[Stmt], returns:
}
/// PT019
fn check_test_function_args(checker: &Checker, parameters: &Parameters) {
fn check_test_function_args(checker: &Checker, parameters: &Parameters, decorators: &[Decorator]) {
let mut named_parametrize = FxHashSet::default();
for decorator in decorators.iter().filter(|decorator| {
UnqualifiedName::from_expr(map_callable(&decorator.expression))
.is_some_and(|name| matches!(name.segments(), ["pytest", "mark", "parametrize"]))
}) {
let Some(call_expr) = decorator.expression.as_call_expr() else {
continue;
};
let Some(first_arg) = call_expr.arguments.find_argument_value("argnames", 0) else {
continue;
};
match first_arg {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
named_parametrize.extend(
value
.to_str()
.split(',')
.map(str::trim)
.filter(|param| !param.is_empty() && param.starts_with('_')),
);
}
Expr::Name(_) => return,
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. })
if elts.iter().any(Expr::is_name_expr) =>
{
return
}
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
named_parametrize.extend(
elts.iter()
.filter_map(Expr::as_string_literal_expr)
.map(|param| param.value.to_str().trim())
.filter(|param| !param.is_empty() && param.starts_with('_')),
);
}
_ => {}
}
}
for parameter in parameters.iter_non_variadic_params() {
let name = parameter.name();
if name.starts_with('_') {
if name.starts_with('_') && !named_parametrize.contains(name.as_str()) {
checker.report_diagnostic(Diagnostic::new(
PytestFixtureParamWithoutValue {
name: name.to_string(),
@@ -915,6 +958,6 @@ pub(crate) fn fixture(
}
if checker.enabled(Rule::PytestFixtureParamWithoutValue) && name.starts_with("test_") {
check_test_function_args(checker, parameters);
check_test_function_args(checker, parameters, decorators);
}
}

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT019.py:9:14: PT019 Fixture `_fixture` without value is injected as parameter, use `@pytest.mark.usefixtures` instead
|
@@ -15,3 +14,19 @@ PT019.py:13:17: PT019 Fixture `_fixture` without value is injected as parameter,
| ^^^^^^^^ PT019
14 | pass
|
PT019.py:31:24: PT019 Fixture `_bar` without value is injected as parameter, use `@pytest.mark.usefixtures` instead
|
30 | @pytest.mark.parametrize("_foo", [1, 2, 3])
31 | def test_thingy2(_foo, _bar): # Error _bar is not defined in parametrize
| ^^^^ PT019
32 | pass
|
PT019.py:39:24: PT019 Fixture `_bar` without value is injected as parameter, use `@pytest.mark.usefixtures` instead
|
38 | @pytest.mark.parametrize(("_foo"), [1, 2, 3])
39 | def test_thingy4(_foo, _bar): # Error _bar is not defined in parametrize
| ^^^^ PT019
40 | pass
|

View File

@@ -83,7 +83,7 @@ pub(crate) fn split_static_string(
let sep_arg = arguments.find_argument_value("sep", 0);
let split_replacement = if let Some(sep) = sep_arg {
match sep {
Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value),
Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value, direction),
Expr::StringLiteral(sep_value) => {
let sep_value_str = sep_value.value.to_str();
Some(split_sep(
@@ -99,7 +99,7 @@ pub(crate) fn split_static_string(
}
}
} else {
split_default(str_value, maxsplit_value)
split_default(str_value, maxsplit_value, direction)
};
let mut diagnostic = Diagnostic::new(SplitStaticString, call.range());
@@ -144,7 +144,11 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
})
}
fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option<Expr> {
fn split_default(
str_value: &StringLiteralValue,
max_split: i32,
direction: Direction,
) -> Option<Expr> {
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
@@ -152,6 +156,7 @@ fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option<Expr>
// > Consequently, splitting an empty string or a string consisting of just whitespace with
// > a None separator returns [].
// https://docs.python.org/3/library/stdtypes.html#str.split
let string_val = str_value.to_str();
match max_split.cmp(&0) {
Ordering::Greater => {
// Autofix for `maxsplit` without separator not yet implemented, as
@@ -160,14 +165,30 @@ fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option<Expr>
None
}
Ordering::Equal => {
let list_items: Vec<&str> = vec![str_value.to_str()];
// Behavior for maxsplit = 0 when sep is None:
// - If the string is empty or all whitespace, result is [].
// - Otherwise:
// - " x ".split(maxsplit=0) -> ['x ']
// - " x ".rsplit(maxsplit=0) -> [' x']
// - "".split(maxsplit=0) -> []
// - " ".split(maxsplit=0) -> []
let processed_str = if direction == Direction::Left {
string_val.trim_start()
} else {
string_val.trim_end()
};
let list_items: &[_] = if processed_str.is_empty() {
&[]
} else {
&[processed_str]
};
Some(construct_replacement(
&list_items,
list_items,
str_value.first_literal_flags(),
))
}
Ordering::Less => {
let list_items: Vec<&str> = str_value.to_str().split_whitespace().collect();
let list_items: Vec<&str> = string_val.split_whitespace().collect();
Some(construct_replacement(
&list_items,
str_value.first_literal_flags(),
@@ -186,12 +207,20 @@ fn split_sep(
let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) {
match direction {
Direction::Left => value.splitn(split_n + 1, sep_value).collect(),
Direction::Right => value.rsplitn(split_n + 1, sep_value).collect(),
Direction::Right => {
let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect();
items.reverse();
items
}
}
} else {
match direction {
Direction::Left => value.split(sep_value).collect(),
Direction::Right => value.rsplit(sep_value).collect(),
Direction::Right => {
let mut items: Vec<&str> = value.rsplit(sep_value).collect();
items.reverse();
items
}
}
};

View File

@@ -352,7 +352,7 @@ SIM905.py:32:1: SIM905 [*] Consider using a list literal instead of `str.split`
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
33 | "".split() # []
34 | """
34 | """
|
= help: Replace with list literal
@@ -363,7 +363,7 @@ SIM905.py:32:1: SIM905 [*] Consider using a list literal instead of `str.split`
32 |-" a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
32 |+[" a", "a a", "a a "] # [" a", "a a", "a a "]
33 33 | "".split() # []
34 34 | """
34 34 | """
35 35 | """.split() # []
SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
@@ -371,7 +371,7 @@ SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 | "".split() # []
| ^^^^^^^^^^ SIM905
34 | """
34 | """
35 | """.split() # []
|
= help: Replace with list literal
@@ -382,7 +382,7 @@ SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 |-"".split() # []
33 |+[] # []
34 34 | """
34 34 | """
35 35 | """.split() # []
36 36 | " ".split() # []
@@ -390,7 +390,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 | "".split() # []
34 | / """
34 | / """
35 | | """.split() # []
| |___________^ SIM905
36 | " ".split() # []
@@ -402,7 +402,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split`
31 31 |
32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 33 | "".split() # []
34 |-"""
34 |-"""
35 |-""".split() # []
34 |+[] # []
36 35 | " ".split() # []
@@ -411,7 +411,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split`
SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
34 | """
34 | """
35 | """.split() # []
36 | " ".split() # []
| ^^^^^^^^^^^^^^^^^ SIM905
@@ -422,7 +422,7 @@ SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split`
Safe fix
33 33 | "".split() # []
34 34 | """
34 34 | """
35 35 | """.split() # []
36 |-" ".split() # []
36 |+[] # []
@@ -442,7 +442,7 @@ SIM905.py:37:1: SIM905 [*] Consider using a list literal instead of `str.split`
= help: Replace with list literal
Safe fix
34 34 | """
34 34 | """
35 35 | """.split() # []
36 36 | " ".split() # []
37 |-"/abc/".split() # ["/abc/"]
@@ -854,6 +854,8 @@ SIM905.py:103:1: SIM905 [*] Consider using a list literal instead of `str.split`
107 | | "'itemD'"
108 | | """.split()
| |___________^ SIM905
109 |
110 | # https://github.com/astral-sh/ruff/issues/18042
|
= help: Replace with list literal
@@ -868,3 +870,393 @@ SIM905.py:103:1: SIM905 [*] Consider using a list literal instead of `str.split`
107 |-"'itemD'"
108 |-""".split()
103 |+['"itemA"', "'itemB'", "'''itemC'''", "\"'itemD'\""]
109 104 |
110 105 | # https://github.com/astral-sh/ruff/issues/18042
111 106 | print("a,b".rsplit(","))
SIM905.py:111:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
110 | # https://github.com/astral-sh/ruff/issues/18042
111 | print("a,b".rsplit(","))
| ^^^^^^^^^^^^^^^^^ SIM905
112 | print("a,b,c".rsplit(",", 1))
|
= help: Replace with list literal
Safe fix
108 108 | """.split()
109 109 |
110 110 | # https://github.com/astral-sh/ruff/issues/18042
111 |-print("a,b".rsplit(","))
111 |+print(["a", "b"])
112 112 | print("a,b,c".rsplit(",", 1))
113 113 |
114 114 | # https://github.com/astral-sh/ruff/issues/18069
SIM905.py:112:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
110 | # https://github.com/astral-sh/ruff/issues/18042
111 | print("a,b".rsplit(","))
112 | print("a,b,c".rsplit(",", 1))
| ^^^^^^^^^^^^^^^^^^^^^^ SIM905
113 |
114 | # https://github.com/astral-sh/ruff/issues/18069
|
= help: Replace with list literal
Safe fix
109 109 |
110 110 | # https://github.com/astral-sh/ruff/issues/18042
111 111 | print("a,b".rsplit(","))
112 |-print("a,b,c".rsplit(",", 1))
112 |+print(["a,b", "c"])
113 113 |
114 114 | # https://github.com/astral-sh/ruff/issues/18069
115 115 |
SIM905.py:116:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
114 | # https://github.com/astral-sh/ruff/issues/18069
115 |
116 | print("".split(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^ SIM905
117 | print("".split(sep=None, maxsplit=0))
118 | print(" ".split(maxsplit=0))
|
= help: Replace with list literal
Safe fix
113 113 |
114 114 | # https://github.com/astral-sh/ruff/issues/18069
115 115 |
116 |-print("".split(maxsplit=0))
116 |+print([])
117 117 | print("".split(sep=None, maxsplit=0))
118 118 | print(" ".split(maxsplit=0))
119 119 | print(" ".split(sep=None, maxsplit=0))
SIM905.py:117:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
116 | print("".split(maxsplit=0))
117 | print("".split(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
118 | print(" ".split(maxsplit=0))
119 | print(" ".split(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
114 114 | # https://github.com/astral-sh/ruff/issues/18069
115 115 |
116 116 | print("".split(maxsplit=0))
117 |-print("".split(sep=None, maxsplit=0))
117 |+print([])
118 118 | print(" ".split(maxsplit=0))
119 119 | print(" ".split(sep=None, maxsplit=0))
120 120 | print(" x ".split(maxsplit=0))
SIM905.py:118:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
116 | print("".split(maxsplit=0))
117 | print("".split(sep=None, maxsplit=0))
118 | print(" ".split(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^ SIM905
119 | print(" ".split(sep=None, maxsplit=0))
120 | print(" x ".split(maxsplit=0))
|
= help: Replace with list literal
Safe fix
115 115 |
116 116 | print("".split(maxsplit=0))
117 117 | print("".split(sep=None, maxsplit=0))
118 |-print(" ".split(maxsplit=0))
118 |+print([])
119 119 | print(" ".split(sep=None, maxsplit=0))
120 120 | print(" x ".split(maxsplit=0))
121 121 | print(" x ".split(sep=None, maxsplit=0))
SIM905.py:119:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
117 | print("".split(sep=None, maxsplit=0))
118 | print(" ".split(maxsplit=0))
119 | print(" ".split(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
120 | print(" x ".split(maxsplit=0))
121 | print(" x ".split(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
116 116 | print("".split(maxsplit=0))
117 117 | print("".split(sep=None, maxsplit=0))
118 118 | print(" ".split(maxsplit=0))
119 |-print(" ".split(sep=None, maxsplit=0))
119 |+print([])
120 120 | print(" x ".split(maxsplit=0))
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 122 | print(" x ".split(maxsplit=0))
SIM905.py:120:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
118 | print(" ".split(maxsplit=0))
119 | print(" ".split(sep=None, maxsplit=0))
120 | print(" x ".split(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM905
121 | print(" x ".split(sep=None, maxsplit=0))
122 | print(" x ".split(maxsplit=0))
|
= help: Replace with list literal
Safe fix
117 117 | print("".split(sep=None, maxsplit=0))
118 118 | print(" ".split(maxsplit=0))
119 119 | print(" ".split(sep=None, maxsplit=0))
120 |-print(" x ".split(maxsplit=0))
120 |+print(["x "])
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 122 | print(" x ".split(maxsplit=0))
123 123 | print(" x ".split(sep=None, maxsplit=0))
SIM905.py:121:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
119 | print(" ".split(sep=None, maxsplit=0))
120 | print(" x ".split(maxsplit=0))
121 | print(" x ".split(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
122 | print(" x ".split(maxsplit=0))
123 | print(" x ".split(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
118 118 | print(" ".split(maxsplit=0))
119 119 | print(" ".split(sep=None, maxsplit=0))
120 120 | print(" x ".split(maxsplit=0))
121 |-print(" x ".split(sep=None, maxsplit=0))
121 |+print(["x "])
122 122 | print(" x ".split(maxsplit=0))
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 124 | print("".rsplit(maxsplit=0))
SIM905.py:122:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
120 | print(" x ".split(maxsplit=0))
121 | print(" x ".split(sep=None, maxsplit=0))
122 | print(" x ".split(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
123 | print(" x ".split(sep=None, maxsplit=0))
124 | print("".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
119 119 | print(" ".split(sep=None, maxsplit=0))
120 120 | print(" x ".split(maxsplit=0))
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 |-print(" x ".split(maxsplit=0))
122 |+print(["x "])
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 124 | print("".rsplit(maxsplit=0))
125 125 | print("".rsplit(sep=None, maxsplit=0))
SIM905.py:123:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
121 | print(" x ".split(sep=None, maxsplit=0))
122 | print(" x ".split(maxsplit=0))
123 | print(" x ".split(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
124 | print("".rsplit(maxsplit=0))
125 | print("".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
120 120 | print(" x ".split(maxsplit=0))
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 122 | print(" x ".split(maxsplit=0))
123 |-print(" x ".split(sep=None, maxsplit=0))
123 |+print(["x "])
124 124 | print("".rsplit(maxsplit=0))
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 126 | print(" ".rsplit(maxsplit=0))
SIM905.py:124:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
122 | print(" x ".split(maxsplit=0))
123 | print(" x ".split(sep=None, maxsplit=0))
124 | print("".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^ SIM905
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 122 | print(" x ".split(maxsplit=0))
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 |-print("".rsplit(maxsplit=0))
124 |+print([])
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 126 | print(" ".rsplit(maxsplit=0))
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
SIM905.py:125:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
123 | print(" x ".split(sep=None, maxsplit=0))
124 | print("".rsplit(maxsplit=0))
125 | print("".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
122 122 | print(" x ".split(maxsplit=0))
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 124 | print("".rsplit(maxsplit=0))
125 |-print("".rsplit(sep=None, maxsplit=0))
125 |+print([])
126 126 | print(" ".rsplit(maxsplit=0))
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 128 | print(" x ".rsplit(maxsplit=0))
SIM905.py:126:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
124 | print("".rsplit(maxsplit=0))
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^ SIM905
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 124 | print("".rsplit(maxsplit=0))
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 |-print(" ".rsplit(maxsplit=0))
126 |+print([])
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 128 | print(" x ".rsplit(maxsplit=0))
129 129 | print(" x ".rsplit(maxsplit=0))
SIM905.py:127:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
124 124 | print("".rsplit(maxsplit=0))
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 126 | print(" ".rsplit(maxsplit=0))
127 |-print(" ".rsplit(sep=None, maxsplit=0))
127 |+print([])
128 128 | print(" x ".rsplit(maxsplit=0))
129 129 | print(" x ".rsplit(maxsplit=0))
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905.py:128:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 126 | print(" ".rsplit(maxsplit=0))
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 |-print(" x ".rsplit(maxsplit=0))
128 |+print([" x"])
129 129 | print(" x ".rsplit(maxsplit=0))
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 131 | print(" x ".rsplit(maxsplit=0))
SIM905.py:129:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
126 126 | print(" ".rsplit(maxsplit=0))
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 128 | print(" x ".rsplit(maxsplit=0))
129 |-print(" x ".rsplit(maxsplit=0))
129 |+print([" x"])
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 131 | print(" x ".rsplit(maxsplit=0))
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905.py:130:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 128 | print(" x ".rsplit(maxsplit=0))
129 129 | print(" x ".rsplit(maxsplit=0))
130 |-print(" x ".rsplit(sep=None, maxsplit=0))
130 |+print([" x"])
131 131 | print(" x ".rsplit(maxsplit=0))
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905.py:131:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
128 128 | print(" x ".rsplit(maxsplit=0))
129 129 | print(" x ".rsplit(maxsplit=0))
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 |-print(" x ".rsplit(maxsplit=0))
131 |+print([" x"])
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
|
= help: Replace with list literal
Safe fix
129 129 | print(" x ".rsplit(maxsplit=0))
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 131 | print(" x ".rsplit(maxsplit=0))
132 |-print(" x ".rsplit(sep=None, maxsplit=0))
132 |+print([" x"])

View File

@@ -26,41 +26,109 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
// PTH100
["os", "path", "abspath"] => OsPathAbspath.into(),
// PTH101
["os", "chmod"] => OsChmod.into(),
["os", "chmod"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod)
// ```text
// 0 1 2 3
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
// ```
if call
.arguments
.find_argument_value("path", 0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|| is_argument_non_default(&call.arguments, "dir_fd", 2)
{
return;
}
OsChmod.into()
}
// PTH102
["os", "makedirs"] => OsMakedirs.into(),
// PTH103
["os", "mkdir"] => OsMkdir.into(),
["os", "mkdir"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.mkdir)
// ```text
// 0 1 2
// os.mkdir(path, mode=0o777, *, dir_fd=None)
// ```
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
return;
}
OsMkdir.into()
}
// PTH104
["os", "rename"] => {
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
// are set to non-default values.
// set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename)
// ```text
// 0 1 2 3
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
// ```
if call
.arguments
.find_argument_value("src_dir_fd", 2)
.is_some_and(|expr| !expr.is_none_literal_expr())
|| call
.arguments
.find_argument_value("dst_dir_fd", 3)
.is_some_and(|expr| !expr.is_none_literal_expr())
if is_argument_non_default(&call.arguments, "src_dir_fd", 2)
|| is_argument_non_default(&call.arguments, "dst_dir_fd", 3)
{
return;
}
OsRename.into()
}
// PTH105
["os", "replace"] => OsReplace.into(),
["os", "replace"] => {
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
// set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace)
// ```text
// 0 1 2 3
// os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
// ```
if is_argument_non_default(&call.arguments, "src_dir_fd", 2)
|| is_argument_non_default(&call.arguments, "dst_dir_fd", 3)
{
return;
}
OsReplace.into()
}
// PTH106
["os", "rmdir"] => OsRmdir.into(),
["os", "rmdir"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rmdir)
// ```text
// 0 1
// os.rmdir(path, *, dir_fd=None)
// ```
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
return;
}
OsRmdir.into()
}
// PTH107
["os", "remove"] => OsRemove.into(),
["os", "remove"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.remove)
// ```text
// 0 1
// os.remove(path, *, dir_fd=None)
// ```
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
return;
}
OsRemove.into()
}
// PTH108
["os", "unlink"] => OsUnlink.into(),
["os", "unlink"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.unlink)
// ```text
// 0 1
// os.unlink(path, *, dir_fd=None)
// ```
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
return;
}
OsUnlink.into()
}
// PTH109
["os", "getcwd"] => OsGetcwd.into(),
["os", "getcwdb"] => OsGetcwd.into(),
@@ -76,10 +144,17 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
["os", "path", "islink"] => OsPathIslink.into(),
// PTH116
["os", "stat"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.stat)
// ```text
// 0 1 2
// os.stat(path, *, dir_fd=None, follow_symlinks=True)
// ```
if call
.arguments
.find_positional(0)
.find_argument_value("path", 0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|| is_argument_non_default(&call.arguments, "dir_fd", 1)
{
return;
}
@@ -148,13 +223,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. })
)
})
|| is_argument_non_default(&call.arguments, "opener", 7)
|| call
.arguments
.find_argument_value("opener", 7)
.is_some_and(|expr| !expr.is_none_literal_expr())
|| call
.arguments
.find_positional(0)
.find_argument_value("file", 0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
{
return;
@@ -164,17 +236,53 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
// PTH124
["py", "path", "local"] => PyPath.into(),
// PTH207
["glob", "glob"] => Glob {
function: "glob".to_string(),
["glob", "glob"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/glob.html#glob.glob)
// ```text
// 0 1 2 3 4
// glob.glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
// ```
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
return;
}
Glob {
function: "glob".to_string(),
}
.into()
}
.into(),
["glob", "iglob"] => Glob {
function: "iglob".to_string(),
["glob", "iglob"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/glob.html#glob.iglob)
// ```text
// 0 1 2 3 4
// glob.iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
// ```
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
return;
}
Glob {
function: "iglob".to_string(),
}
.into()
}
.into(),
// PTH115
// Python 3.9+
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => OsReadlink.into(),
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.readlink)
// ```text
// 0 1
// os.readlink(path, *, dir_fd=None)
// ```
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
return;
}
OsReadlink.into()
}
// PTH208
["os", "listdir"] => {
if call
@@ -224,3 +332,10 @@ fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
_ => None,
}
}
/// Returns `true` if argument `name` is set to a non-default `None` value.
fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usize) -> bool {
arguments
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())
}

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
snapshot_kind: text
---
PTH207.py:9:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob`
|
@@ -26,4 +25,6 @@ PTH207.py:11:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob`
10 | list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp")))
11 | search("*.png")
| ^^^^^^ PTH207
12 |
13 | # if `dir_fd` is set, suppress the diagnostic
|

View File

@@ -93,7 +93,7 @@ PERF401.py:142:9: PERF401 Use a list comprehension to create a transformed list
PERF401.py:149:9: PERF401 Use a list comprehension to create a transformed list
|
147 | tmp = 1; result = [] # commment should be protected
147 | tmp = 1; result = [] # comment should be protected
148 | for i in range(10):
149 | result.append(i + 1) # PERF401
| ^^^^^^^^^^^^^^^^^^^^ PERF401
@@ -102,7 +102,7 @@ PERF401.py:149:9: PERF401 Use a list comprehension to create a transformed list
PERF401.py:156:9: PERF401 Use a list comprehension to create a transformed list
|
154 | result = []; tmp = 1 # commment should be protected
154 | result = []; tmp = 1 # comment should be protected
155 | for i in range(10):
156 | result.append(i + 1) # PERF401
| ^^^^^^^^^^^^^^^^^^^^ PERF401

View File

@@ -223,7 +223,7 @@ PERF401.py:142:9: PERF401 [*] Use a list comprehension to create a transformed l
PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed list
|
147 | tmp = 1; result = [] # commment should be protected
147 | tmp = 1; result = [] # comment should be protected
148 | for i in range(10):
149 | result.append(i + 1) # PERF401
| ^^^^^^^^^^^^^^^^^^^^ PERF401
@@ -234,10 +234,10 @@ PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed l
144 144 |
145 145 | def f():
146 146 | # make sure that `tmp` is not deleted
147 |- tmp = 1; result = [] # commment should be protected
147 |- tmp = 1; result = [] # comment should be protected
148 |- for i in range(10):
149 |- result.append(i + 1) # PERF401
147 |+ tmp = 1 # commment should be protected
147 |+ tmp = 1 # comment should be protected
148 |+ result = [i + 1 for i in range(10)] # PERF401
150 149 |
151 150 |
@@ -245,7 +245,7 @@ PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed l
PERF401.py:156:9: PERF401 [*] Use a list comprehension to create a transformed list
|
154 | result = []; tmp = 1 # commment should be protected
154 | result = []; tmp = 1 # comment should be protected
155 | for i in range(10):
156 | result.append(i + 1) # PERF401
| ^^^^^^^^^^^^^^^^^^^^ PERF401
@@ -256,10 +256,10 @@ PERF401.py:156:9: PERF401 [*] Use a list comprehension to create a transformed l
151 151 |
152 152 | def f():
153 153 | # make sure that `tmp` is not deleted
154 |- result = []; tmp = 1 # commment should be protected
154 |- result = []; tmp = 1 # comment should be protected
155 |- for i in range(10):
156 |- result.append(i + 1) # PERF401
154 |+ tmp = 1 # commment should be protected
154 |+ tmp = 1 # comment should be protected
155 |+ result = [i + 1 for i in range(10)] # PERF401
157 156 |
158 157 |

View File

@@ -38,6 +38,11 @@ use crate::checkers::ast::Checker;
/// nums.add(num + 5)
/// ```
///
/// ## Fix safety
/// This fix is always unsafe because it changes the programs behavior. Replacing the
/// original set with a copy during iteration allows code that would previously raise a
/// `RuntimeError` to run without error.
///
/// ## References
/// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set)
#[derive(ViolationMetadata)]

View File

@@ -21,6 +21,7 @@ pub(crate) enum MinMax {
/// readability.
///
/// ## Example
///
/// ```python
/// minimum = min(1, 2, min(3, 4, 5))
/// maximum = max(1, 2, max(3, 4, 5))
@@ -28,12 +29,26 @@ pub(crate) enum MinMax {
/// ```
///
/// Use instead:
///
/// ```python
/// minimum = min(1, 2, 3, 4, 5)
/// maximum = max(1, 2, 3, 4, 5)
/// diff = maximum - minimum
/// ```
///
/// ## Fix safety
///
/// This fix is always unsafe and may change the program's behavior for types without full
/// equivalence relations, such as float comparisons involving `NaN`.
///
/// ```python
/// print(min(2.0, min(float("nan"), 1.0))) # before fix: 2.0
/// print(min(2.0, float("nan"), 1.0)) # after fix: 1.0
///
/// print(max(1.0, max(float("nan"), 2.0))) # before fix: 1.0
/// print(max(1.0, float("nan"), 2.0)) # after fix: 2.0
/// ```
///
/// ## References
/// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min)
/// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max)

View File

@@ -28,6 +28,17 @@ use crate::fix::edits::add_argument;
/// Python 3.10 and later, or `locale.getpreferredencoding()` on earlier versions,
/// to make the encoding explicit.
///
/// ## Fix safety
/// This fix is always unsafe and may change the program's behavior. It forces
/// `encoding="utf-8"` as the default, regardless of the platforms actual default
/// encoding, which may cause `UnicodeDecodeError` on non-UTF-8 systems.
/// ```python
/// with open("test.txt") as f:
/// print(f.read()) # before fix (on UTF-8 systems): 你好,世界!
/// with open("test.txt", encoding="utf-8") as f:
/// print(f.read()) # after fix (on Windows): UnicodeDecodeError
/// ```
///
/// ## Example
/// ```python
/// open("file.txt")

View File

@@ -12,6 +12,11 @@ use crate::checkers::ast::Checker;
/// ## Why is this bad?
/// The import alias is redundant and should be removed to avoid confusion.
///
/// ## Fix safety
/// This fix is marked as always unsafe because the user may be intentionally
/// re-exporting the import. While statements like `import numpy as numpy`
/// appear redundant, they can have semantic meaning in certain contexts.
///
/// ## Example
/// ```python
/// import numpy as numpy

View File

@@ -64,7 +64,7 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool {
[
"" | "builtins",
"EnvironmentError" | "IOError" | "WindowsError"
] | ["mmap" | "select" | "socket" | "os", "error"]
] | ["mmap" | "resource" | "select" | "socket" | "os", "error"]
)
})
}

View File

@@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
UP024_2.py:10:7: UP024 [*] Replace aliased errors with `OSError`
|
8 | # Testing the modules
9 | import socket, mmap, select
9 | import socket, mmap, select, resource
10 | raise socket.error
| ^^^^^^^^^^^^ UP024
11 | raise mmap.error
@@ -15,32 +15,33 @@ UP024_2.py:10:7: UP024 [*] Replace aliased errors with `OSError`
Safe fix
7 7 |
8 8 | # Testing the modules
9 9 | import socket, mmap, select
9 9 | import socket, mmap, select, resource
10 |-raise socket.error
10 |+raise OSError
11 11 | raise mmap.error
12 12 | raise select.error
13 13 |
13 13 | raise resource.error
UP024_2.py:11:7: UP024 [*] Replace aliased errors with `OSError`
|
9 | import socket, mmap, select
9 | import socket, mmap, select, resource
10 | raise socket.error
11 | raise mmap.error
| ^^^^^^^^^^ UP024
12 | raise select.error
13 | raise resource.error
|
= help: Replace `mmap.error` with builtin `OSError`
Safe fix
8 8 | # Testing the modules
9 9 | import socket, mmap, select
9 9 | import socket, mmap, select, resource
10 10 | raise socket.error
11 |-raise mmap.error
11 |+raise OSError
12 12 | raise select.error
13 13 |
14 14 | raise socket.error()
13 13 | raise resource.error
14 14 |
UP024_2.py:12:7: UP024 [*] Replace aliased errors with `OSError`
|
@@ -48,355 +49,416 @@ UP024_2.py:12:7: UP024 [*] Replace aliased errors with `OSError`
11 | raise mmap.error
12 | raise select.error
| ^^^^^^^^^^^^ UP024
13 |
14 | raise socket.error()
13 | raise resource.error
|
= help: Replace `select.error` with builtin `OSError`
Safe fix
9 9 | import socket, mmap, select
9 9 | import socket, mmap, select, resource
10 10 | raise socket.error
11 11 | raise mmap.error
12 |-raise select.error
12 |+raise OSError
13 13 |
14 14 | raise socket.error()
15 15 | raise mmap.error(1)
13 13 | raise resource.error
14 14 |
15 15 | raise socket.error()
UP024_2.py:14:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:13:7: UP024 [*] Replace aliased errors with `OSError`
|
11 | raise mmap.error
12 | raise select.error
13 |
14 | raise socket.error()
13 | raise resource.error
| ^^^^^^^^^^^^^^ UP024
14 |
15 | raise socket.error()
|
= help: Replace `resource.error` with builtin `OSError`
Safe fix
10 10 | raise socket.error
11 11 | raise mmap.error
12 12 | raise select.error
13 |-raise resource.error
13 |+raise OSError
14 14 |
15 15 | raise socket.error()
16 16 | raise mmap.error(1)
UP024_2.py:15:7: UP024 [*] Replace aliased errors with `OSError`
|
13 | raise resource.error
14 |
15 | raise socket.error()
| ^^^^^^^^^^^^ UP024
15 | raise mmap.error(1)
16 | raise select.error(1, 2)
16 | raise mmap.error(1)
17 | raise select.error(1, 2)
|
= help: Replace `socket.error` with builtin `OSError`
Safe fix
11 11 | raise mmap.error
12 12 | raise select.error
13 13 |
14 |-raise socket.error()
14 |+raise OSError()
15 15 | raise mmap.error(1)
16 16 | raise select.error(1, 2)
17 17 |
13 13 | raise resource.error
14 14 |
15 |-raise socket.error()
15 |+raise OSError()
16 16 | raise mmap.error(1)
17 17 | raise select.error(1, 2)
18 18 | raise resource.error(1, "strerror", "filename")
UP024_2.py:15:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:16:7: UP024 [*] Replace aliased errors with `OSError`
|
14 | raise socket.error()
15 | raise mmap.error(1)
15 | raise socket.error()
16 | raise mmap.error(1)
| ^^^^^^^^^^ UP024
16 | raise select.error(1, 2)
17 | raise select.error(1, 2)
18 | raise resource.error(1, "strerror", "filename")
|
= help: Replace `mmap.error` with builtin `OSError`
Safe fix
12 12 | raise select.error
13 13 |
14 14 | raise socket.error()
15 |-raise mmap.error(1)
15 |+raise OSError(1)
16 16 | raise select.error(1, 2)
17 17 |
18 18 | raise socket.error(
13 13 | raise resource.error
14 14 |
15 15 | raise socket.error()
16 |-raise mmap.error(1)
16 |+raise OSError(1)
17 17 | raise select.error(1, 2)
18 18 | raise resource.error(1, "strerror", "filename")
19 19 |
UP024_2.py:16:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:17:7: UP024 [*] Replace aliased errors with `OSError`
|
14 | raise socket.error()
15 | raise mmap.error(1)
16 | raise select.error(1, 2)
15 | raise socket.error()
16 | raise mmap.error(1)
17 | raise select.error(1, 2)
| ^^^^^^^^^^^^ UP024
17 |
18 | raise socket.error(
18 | raise resource.error(1, "strerror", "filename")
|
= help: Replace `select.error` with builtin `OSError`
Safe fix
13 13 |
14 14 | raise socket.error()
15 15 | raise mmap.error(1)
16 |-raise select.error(1, 2)
16 |+raise OSError(1, 2)
17 17 |
18 18 | raise socket.error(
19 19 | 1,
14 14 |
15 15 | raise socket.error()
16 16 | raise mmap.error(1)
17 |-raise select.error(1, 2)
17 |+raise OSError(1, 2)
18 18 | raise resource.error(1, "strerror", "filename")
19 19 |
20 20 | raise socket.error(
UP024_2.py:18:7: UP024 [*] Replace aliased errors with `OSError`
|
16 | raise select.error(1, 2)
17 |
18 | raise socket.error(
16 | raise mmap.error(1)
17 | raise select.error(1, 2)
18 | raise resource.error(1, "strerror", "filename")
| ^^^^^^^^^^^^^^ UP024
19 |
20 | raise socket.error(
|
= help: Replace `resource.error` with builtin `OSError`
Safe fix
15 15 | raise socket.error()
16 16 | raise mmap.error(1)
17 17 | raise select.error(1, 2)
18 |-raise resource.error(1, "strerror", "filename")
18 |+raise OSError(1, "strerror", "filename")
19 19 |
20 20 | raise socket.error(
21 21 | 1,
UP024_2.py:20:7: UP024 [*] Replace aliased errors with `OSError`
|
18 | raise resource.error(1, "strerror", "filename")
19 |
20 | raise socket.error(
| ^^^^^^^^^^^^ UP024
19 | 1,
20 | 2,
21 | 1,
22 | 2,
|
= help: Replace `socket.error` with builtin `OSError`
Safe fix
15 15 | raise mmap.error(1)
16 16 | raise select.error(1, 2)
17 17 |
18 |-raise socket.error(
18 |+raise OSError(
19 19 | 1,
20 20 | 2,
21 21 | 3,
17 17 | raise select.error(1, 2)
18 18 | raise resource.error(1, "strerror", "filename")
19 19 |
20 |-raise socket.error(
20 |+raise OSError(
21 21 | 1,
22 22 | 2,
23 23 | 3,
UP024_2.py:25:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:27:7: UP024 [*] Replace aliased errors with `OSError`
|
24 | from mmap import error
25 | raise error
26 | from mmap import error
27 | raise error
| ^^^^^ UP024
26 |
27 | from socket import error
28 |
29 | from socket import error
|
= help: Replace `error` with builtin `OSError`
Safe fix
22 22 | )
23 23 |
24 24 | from mmap import error
25 |-raise error
25 |+raise OSError
26 26 |
27 27 | from socket import error
28 28 | raise error(1)
24 24 | )
25 25 |
26 26 | from mmap import error
27 |-raise error
27 |+raise OSError
28 28 |
29 29 | from socket import error
30 30 | raise error(1)
UP024_2.py:28:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:30:7: UP024 [*] Replace aliased errors with `OSError`
|
27 | from socket import error
28 | raise error(1)
29 | from socket import error
30 | raise error(1)
| ^^^^^ UP024
29 |
30 | from select import error
31 |
32 | from select import error
|
= help: Replace `error` with builtin `OSError`
Safe fix
25 25 | raise error
26 26 |
27 27 | from socket import error
28 |-raise error(1)
28 |+raise OSError(1)
29 29 |
30 30 | from select import error
31 31 | raise error(1, 2)
27 27 | raise error
28 28 |
29 29 | from socket import error
30 |-raise error(1)
30 |+raise OSError(1)
31 31 |
32 32 | from select import error
33 33 | raise error(1, 2)
UP024_2.py:31:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:33:7: UP024 [*] Replace aliased errors with `OSError`
|
30 | from select import error
31 | raise error(1, 2)
32 | from select import error
33 | raise error(1, 2)
| ^^^^^ UP024
32 |
33 | # Testing the names
34 |
35 | from resource import error
|
= help: Replace `error` with builtin `OSError`
Safe fix
28 28 | raise error(1)
29 29 |
30 30 | from select import error
31 |-raise error(1, 2)
31 |+raise OSError(1, 2)
32 32 |
33 33 | # Testing the names
34 34 | raise EnvironmentError
UP024_2.py:34:7: UP024 [*] Replace aliased errors with `OSError`
|
33 | # Testing the names
34 | raise EnvironmentError
| ^^^^^^^^^^^^^^^^ UP024
35 | raise IOError
36 | raise WindowsError
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
31 31 | raise error(1, 2)
32 32 |
33 33 | # Testing the names
34 |-raise EnvironmentError
34 |+raise OSError
35 35 | raise IOError
36 36 | raise WindowsError
37 37 |
UP024_2.py:35:7: UP024 [*] Replace aliased errors with `OSError`
|
33 | # Testing the names
34 | raise EnvironmentError
35 | raise IOError
| ^^^^^^^ UP024
36 | raise WindowsError
|
= help: Replace `IOError` with builtin `OSError`
Safe fix
32 32 |
33 33 | # Testing the names
34 34 | raise EnvironmentError
35 |-raise IOError
35 |+raise OSError
36 36 | raise WindowsError
37 37 |
38 38 | raise EnvironmentError()
30 30 | raise error(1)
31 31 |
32 32 | from select import error
33 |-raise error(1, 2)
33 |+raise OSError(1, 2)
34 34 |
35 35 | from resource import error
36 36 | raise error(1, "strerror", "filename")
UP024_2.py:36:7: UP024 [*] Replace aliased errors with `OSError`
|
34 | raise EnvironmentError
35 | raise IOError
36 | raise WindowsError
| ^^^^^^^^^^^^ UP024
35 | from resource import error
36 | raise error(1, "strerror", "filename")
| ^^^^^ UP024
37 |
38 | raise EnvironmentError()
38 | # Testing the names
|
= help: Replace `WindowsError` with builtin `OSError`
= help: Replace `error` with builtin `OSError`
Safe fix
33 33 | # Testing the names
34 34 | raise EnvironmentError
35 35 | raise IOError
36 |-raise WindowsError
36 |+raise OSError
33 33 | raise error(1, 2)
34 34 |
35 35 | from resource import error
36 |-raise error(1, "strerror", "filename")
36 |+raise OSError(1, "strerror", "filename")
37 37 |
38 38 | raise EnvironmentError()
39 39 | raise IOError(1)
UP024_2.py:38:7: UP024 [*] Replace aliased errors with `OSError`
|
36 | raise WindowsError
37 |
38 | raise EnvironmentError()
| ^^^^^^^^^^^^^^^^ UP024
39 | raise IOError(1)
40 | raise WindowsError(1, 2)
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
35 35 | raise IOError
36 36 | raise WindowsError
37 37 |
38 |-raise EnvironmentError()
38 |+raise OSError()
39 39 | raise IOError(1)
40 40 | raise WindowsError(1, 2)
41 41 |
38 38 | # Testing the names
39 39 | raise EnvironmentError
UP024_2.py:39:7: UP024 [*] Replace aliased errors with `OSError`
|
38 | raise EnvironmentError()
39 | raise IOError(1)
| ^^^^^^^ UP024
40 | raise WindowsError(1, 2)
38 | # Testing the names
39 | raise EnvironmentError
| ^^^^^^^^^^^^^^^^ UP024
40 | raise IOError
41 | raise WindowsError
|
= help: Replace `IOError` with builtin `OSError`
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
36 36 | raise WindowsError
36 36 | raise error(1, "strerror", "filename")
37 37 |
38 38 | raise EnvironmentError()
39 |-raise IOError(1)
39 |+raise OSError(1)
40 40 | raise WindowsError(1, 2)
41 41 |
42 42 | raise EnvironmentError(
38 38 | # Testing the names
39 |-raise EnvironmentError
39 |+raise OSError
40 40 | raise IOError
41 41 | raise WindowsError
42 42 |
UP024_2.py:40:7: UP024 [*] Replace aliased errors with `OSError`
|
38 | raise EnvironmentError()
39 | raise IOError(1)
40 | raise WindowsError(1, 2)
| ^^^^^^^^^^^^ UP024
41 |
42 | raise EnvironmentError(
38 | # Testing the names
39 | raise EnvironmentError
40 | raise IOError
| ^^^^^^^ UP024
41 | raise WindowsError
|
= help: Replace `WindowsError` with builtin `OSError`
= help: Replace `IOError` with builtin `OSError`
Safe fix
37 37 |
38 38 | raise EnvironmentError()
39 39 | raise IOError(1)
40 |-raise WindowsError(1, 2)
40 |+raise OSError(1, 2)
41 41 |
42 42 | raise EnvironmentError(
43 43 | 1,
38 38 | # Testing the names
39 39 | raise EnvironmentError
40 |-raise IOError
40 |+raise OSError
41 41 | raise WindowsError
42 42 |
43 43 | raise EnvironmentError()
UP024_2.py:42:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:41:7: UP024 [*] Replace aliased errors with `OSError`
|
40 | raise WindowsError(1, 2)
41 |
42 | raise EnvironmentError(
| ^^^^^^^^^^^^^^^^ UP024
43 | 1,
44 | 2,
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
39 39 | raise IOError(1)
40 40 | raise WindowsError(1, 2)
41 41 |
42 |-raise EnvironmentError(
42 |+raise OSError(
43 43 | 1,
44 44 | 2,
45 45 | 3,
UP024_2.py:48:7: UP024 [*] Replace aliased errors with `OSError`
|
46 | )
47 |
48 | raise WindowsError
39 | raise EnvironmentError
40 | raise IOError
41 | raise WindowsError
| ^^^^^^^^^^^^ UP024
49 | raise EnvironmentError(1)
50 | raise IOError(1, 2)
42 |
43 | raise EnvironmentError()
|
= help: Replace `WindowsError` with builtin `OSError`
Safe fix
45 45 | 3,
46 46 | )
47 47 |
48 |-raise WindowsError
48 |+raise OSError
49 49 | raise EnvironmentError(1)
50 50 | raise IOError(1, 2)
38 38 | # Testing the names
39 39 | raise EnvironmentError
40 40 | raise IOError
41 |-raise WindowsError
41 |+raise OSError
42 42 |
43 43 | raise EnvironmentError()
44 44 | raise IOError(1)
UP024_2.py:49:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:43:7: UP024 [*] Replace aliased errors with `OSError`
|
48 | raise WindowsError
49 | raise EnvironmentError(1)
41 | raise WindowsError
42 |
43 | raise EnvironmentError()
| ^^^^^^^^^^^^^^^^ UP024
50 | raise IOError(1, 2)
44 | raise IOError(1)
45 | raise WindowsError(1, 2)
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
46 46 | )
47 47 |
48 48 | raise WindowsError
49 |-raise EnvironmentError(1)
49 |+raise OSError(1)
50 50 | raise IOError(1, 2)
40 40 | raise IOError
41 41 | raise WindowsError
42 42 |
43 |-raise EnvironmentError()
43 |+raise OSError()
44 44 | raise IOError(1)
45 45 | raise WindowsError(1, 2)
46 46 |
UP024_2.py:50:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:44:7: UP024 [*] Replace aliased errors with `OSError`
|
48 | raise WindowsError
49 | raise EnvironmentError(1)
50 | raise IOError(1, 2)
43 | raise EnvironmentError()
44 | raise IOError(1)
| ^^^^^^^ UP024
45 | raise WindowsError(1, 2)
|
= help: Replace `IOError` with builtin `OSError`
Safe fix
41 41 | raise WindowsError
42 42 |
43 43 | raise EnvironmentError()
44 |-raise IOError(1)
44 |+raise OSError(1)
45 45 | raise WindowsError(1, 2)
46 46 |
47 47 | raise EnvironmentError(
UP024_2.py:45:7: UP024 [*] Replace aliased errors with `OSError`
|
43 | raise EnvironmentError()
44 | raise IOError(1)
45 | raise WindowsError(1, 2)
| ^^^^^^^^^^^^ UP024
46 |
47 | raise EnvironmentError(
|
= help: Replace `WindowsError` with builtin `OSError`
Safe fix
42 42 |
43 43 | raise EnvironmentError()
44 44 | raise IOError(1)
45 |-raise WindowsError(1, 2)
45 |+raise OSError(1, 2)
46 46 |
47 47 | raise EnvironmentError(
48 48 | 1,
UP024_2.py:47:7: UP024 [*] Replace aliased errors with `OSError`
|
45 | raise WindowsError(1, 2)
46 |
47 | raise EnvironmentError(
| ^^^^^^^^^^^^^^^^ UP024
48 | 1,
49 | 2,
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
44 44 | raise IOError(1)
45 45 | raise WindowsError(1, 2)
46 46 |
47 |-raise EnvironmentError(
47 |+raise OSError(
48 48 | 1,
49 49 | 2,
50 50 | 3,
UP024_2.py:53:7: UP024 [*] Replace aliased errors with `OSError`
|
51 | )
52 |
53 | raise WindowsError
| ^^^^^^^^^^^^ UP024
54 | raise EnvironmentError(1)
55 | raise IOError(1, 2)
|
= help: Replace `WindowsError` with builtin `OSError`
Safe fix
50 50 | 3,
51 51 | )
52 52 |
53 |-raise WindowsError
53 |+raise OSError
54 54 | raise EnvironmentError(1)
55 55 | raise IOError(1, 2)
UP024_2.py:54:7: UP024 [*] Replace aliased errors with `OSError`
|
53 | raise WindowsError
54 | raise EnvironmentError(1)
| ^^^^^^^^^^^^^^^^ UP024
55 | raise IOError(1, 2)
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
51 51 | )
52 52 |
53 53 | raise WindowsError
54 |-raise EnvironmentError(1)
54 |+raise OSError(1)
55 55 | raise IOError(1, 2)
UP024_2.py:55:7: UP024 [*] Replace aliased errors with `OSError`
|
53 | raise WindowsError
54 | raise EnvironmentError(1)
55 | raise IOError(1, 2)
| ^^^^^^^ UP024
|
= help: Replace `IOError` with builtin `OSError`
Safe fix
47 47 |
48 48 | raise WindowsError
49 49 | raise EnvironmentError(1)
50 |-raise IOError(1, 2)
50 |+raise OSError(1, 2)
52 52 |
53 53 | raise WindowsError
54 54 | raise EnvironmentError(1)
55 |-raise IOError(1, 2)
55 |+raise OSError(1, 2)

View File

@@ -89,4 +89,15 @@ mod tests {
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn fstring_number_format_python_311() -> Result<()> {
let diagnostics = test_path(
Path::new("refurb/FURB116.py"),
&settings::LinterSettings::for_rule(Rule::FStringNumberFormat)
.with_target_version(PythonVersion::PY311),
)?;
assert_messages!(diagnostics);
Ok(())
}
}

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr, ExprCall, Number};
use ruff_python_ast::{self as ast, Expr, ExprCall, Number, PythonVersion, UnaryOp};
use ruff_source_file::find_newline;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -24,6 +25,11 @@ use crate::fix::snippet::SourceCodeSnippet;
/// ```python
/// print(f"{1337:b}")
/// ```
///
/// ## Fix safety
/// The fix is only marked as safe for integer literals, all other cases
/// are display-only, as they may change the runtime behaviour of the program
/// or introduce syntax errors.
#[derive(ViolationMetadata)]
pub(crate) struct FStringNumberFormat {
replacement: Option<SourceCodeSnippet>,
@@ -121,21 +127,24 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs
return;
}
// Generate a replacement, if possible.
let replacement = if matches!(
arg,
Expr::NumberLiteral(_) | Expr::Name(_) | Expr::Attribute(_)
) {
let inner_source = checker.locator().slice(arg);
let quote = checker.stylist().quote();
let shorthand = base.shorthand();
Some(format!("f{quote}{{{inner_source}:{shorthand}}}{quote}"))
let maybe_number = if let Some(maybe_number) = arg
.as_unary_op_expr()
.filter(|unary_expr| unary_expr.op == UnaryOp::UAdd)
.map(|unary_expr| &unary_expr.operand)
{
maybe_number
} else {
None
arg
};
let applicability = if matches!(maybe_number, Expr::NumberLiteral(_)) {
Applicability::Safe
} else {
Applicability::DisplayOnly
};
let replacement = try_create_replacement(checker, arg, base);
let mut diagnostic = Diagnostic::new(
FStringNumberFormat {
replacement: replacement.as_deref().map(SourceCodeSnippet::from_str),
@@ -145,15 +154,54 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs
);
if let Some(replacement) = replacement {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
replacement,
subscript.range(),
)));
let edit = Edit::range_replacement(replacement, subscript.range());
diagnostic.set_fix(Fix::applicable_edit(edit, applicability));
}
checker.report_diagnostic(diagnostic);
}
/// Generate a replacement, if possible.
fn try_create_replacement(checker: &Checker, arg: &Expr, base: Base) -> Option<String> {
if !matches!(
arg,
Expr::NumberLiteral(_) | Expr::Name(_) | Expr::Attribute(_) | Expr::UnaryOp(_)
) {
return None;
}
let inner_source = checker.locator().slice(arg);
// On Python 3.11 and earlier, trying to replace an `arg` that contains a backslash
// would create a `SyntaxError` in the f-string.
if checker.target_version() <= PythonVersion::PY311 && inner_source.contains('\\') {
return None;
}
// On Python 3.11 and earlier, trying to replace an `arg` that spans multiple lines
// would create a `SyntaxError` in the f-string.
if checker.target_version() <= PythonVersion::PY311 && find_newline(inner_source).is_some() {
return None;
}
let quote = checker.stylist().quote();
let shorthand = base.shorthand();
// If the `arg` contains double quotes we need to create the f-string with single quotes
// to avoid a `SyntaxError` in Python 3.11 and earlier.
if checker.target_version() <= PythonVersion::PY311 && inner_source.contains(quote.as_str()) {
return None;
}
// If the `arg` contains a brace add an space before it to avoid a `SyntaxError`
// in the f-string.
if inner_source.starts_with('{') {
Some(format!("f{quote}{{ {inner_source}:{shorthand}}}{quote}"))
} else {
Some(format!("f{quote}{{{inner_source}:{shorthand}}}{quote}"))
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Base {
Hex,

View File

@@ -1,144 +1,288 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB116.py:6:7: FURB116 [*] Replace `oct` call with `f"{num:o}"`
|
4 | return num
5 |
6 | print(oct(num)[2:]) # FURB116
| ^^^^^^^^^^^^ FURB116
7 | print(hex(num)[2:]) # FURB116
8 | print(bin(num)[2:]) # FURB116
|
= help: Replace with `f"{num:o}"`
Safe fix
3 3 | def return_num() -> int:
4 4 | return num
5 5 |
6 |-print(oct(num)[2:]) # FURB116
6 |+print(f"{num:o}") # FURB116
7 7 | print(hex(num)[2:]) # FURB116
8 8 | print(bin(num)[2:]) # FURB116
9 9 |
FURB116.py:7:7: FURB116 [*] Replace `hex` call with `f"{num:x}"`
|
6 | print(oct(num)[2:]) # FURB116
7 | print(hex(num)[2:]) # FURB116
| ^^^^^^^^^^^^ FURB116
8 | print(bin(num)[2:]) # FURB116
|
= help: Replace with `f"{num:x}"`
Safe fix
4 4 | return num
5 5 |
6 6 | print(oct(num)[2:]) # FURB116
7 |-print(hex(num)[2:]) # FURB116
7 |+print(f"{num:x}") # FURB116
8 8 | print(bin(num)[2:]) # FURB116
9 9 |
10 10 | print(oct(1337)[2:]) # FURB116
FURB116.py:8:7: FURB116 [*] Replace `bin` call with `f"{num:b}"`
FURB116.py:9:7: FURB116 Replace `oct` call with `f"{num:o}"`
|
6 | print(oct(num)[2:]) # FURB116
7 | print(hex(num)[2:]) # FURB116
8 | print(bin(num)[2:]) # FURB116
7 | return num
8 |
9 | print(oct(num)[2:]) # FURB116
| ^^^^^^^^^^^^ FURB116
9 |
10 | print(oct(1337)[2:]) # FURB116
10 | print(hex(num)[2:]) # FURB116
11 | print(bin(num)[2:]) # FURB116
|
= help: Replace with `f"{num:o}"`
Display-only fix
6 6 | def return_num() -> int:
7 7 | return num
8 8 |
9 |-print(oct(num)[2:]) # FURB116
9 |+print(f"{num:o}") # FURB116
10 10 | print(hex(num)[2:]) # FURB116
11 11 | print(bin(num)[2:]) # FURB116
12 12 |
FURB116.py:10:7: FURB116 Replace `hex` call with `f"{num:x}"`
|
9 | print(oct(num)[2:]) # FURB116
10 | print(hex(num)[2:]) # FURB116
| ^^^^^^^^^^^^ FURB116
11 | print(bin(num)[2:]) # FURB116
|
= help: Replace with `f"{num:x}"`
Display-only fix
7 7 | return num
8 8 |
9 9 | print(oct(num)[2:]) # FURB116
10 |-print(hex(num)[2:]) # FURB116
10 |+print(f"{num:x}") # FURB116
11 11 | print(bin(num)[2:]) # FURB116
12 12 |
13 13 | print(oct(1337)[2:]) # FURB116
FURB116.py:11:7: FURB116 Replace `bin` call with `f"{num:b}"`
|
9 | print(oct(num)[2:]) # FURB116
10 | print(hex(num)[2:]) # FURB116
11 | print(bin(num)[2:]) # FURB116
| ^^^^^^^^^^^^ FURB116
12 |
13 | print(oct(1337)[2:]) # FURB116
|
= help: Replace with `f"{num:b}"`
Safe fix
5 5 |
6 6 | print(oct(num)[2:]) # FURB116
7 7 | print(hex(num)[2:]) # FURB116
8 |-print(bin(num)[2:]) # FURB116
8 |+print(f"{num:b}") # FURB116
9 9 |
10 10 | print(oct(1337)[2:]) # FURB116
11 11 | print(hex(1337)[2:]) # FURB116
Display-only fix
8 8 |
9 9 | print(oct(num)[2:]) # FURB116
10 10 | print(hex(num)[2:]) # FURB116
11 |-print(bin(num)[2:]) # FURB116
11 |+print(f"{num:b}") # FURB116
12 12 |
13 13 | print(oct(1337)[2:]) # FURB116
14 14 | print(hex(1337)[2:]) # FURB116
FURB116.py:10:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
FURB116.py:13:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
|
8 | print(bin(num)[2:]) # FURB116
9 |
10 | print(oct(1337)[2:]) # FURB116
11 | print(bin(num)[2:]) # FURB116
12 |
13 | print(oct(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
11 | print(hex(1337)[2:]) # FURB116
12 | print(bin(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:o}"`
Safe fix
7 7 | print(hex(num)[2:]) # FURB116
8 8 | print(bin(num)[2:]) # FURB116
9 9 |
10 |-print(oct(1337)[2:]) # FURB116
10 |+print(f"{1337:o}") # FURB116
11 11 | print(hex(1337)[2:]) # FURB116
12 12 | print(bin(1337)[2:]) # FURB116
13 13 |
10 10 | print(hex(num)[2:]) # FURB116
11 11 | print(bin(num)[2:]) # FURB116
12 12 |
13 |-print(oct(1337)[2:]) # FURB116
13 |+print(f"{1337:o}") # FURB116
14 14 | print(hex(1337)[2:]) # FURB116
15 15 | print(bin(1337)[2:]) # FURB116
16 16 | print(bin(+1337)[2:]) # FURB116
FURB116.py:11:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
FURB116.py:14:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
|
10 | print(oct(1337)[2:]) # FURB116
11 | print(hex(1337)[2:]) # FURB116
13 | print(oct(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
12 | print(bin(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
16 | print(bin(+1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:x}"`
Safe fix
8 8 | print(bin(num)[2:]) # FURB116
9 9 |
10 10 | print(oct(1337)[2:]) # FURB116
11 |-print(hex(1337)[2:]) # FURB116
11 |+print(f"{1337:x}") # FURB116
12 12 | print(bin(1337)[2:]) # FURB116
13 13 |
14 14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
11 11 | print(bin(num)[2:]) # FURB116
12 12 |
13 13 | print(oct(1337)[2:]) # FURB116
14 |-print(hex(1337)[2:]) # FURB116
14 |+print(f"{1337:x}") # FURB116
15 15 | print(bin(1337)[2:]) # FURB116
16 16 | print(bin(+1337)[2:]) # FURB116
17 17 |
FURB116.py:12:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
FURB116.py:15:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
|
10 | print(oct(1337)[2:]) # FURB116
11 | print(hex(1337)[2:]) # FURB116
12 | print(bin(1337)[2:]) # FURB116
13 | print(oct(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
13 |
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
16 | print(bin(+1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:b}"`
Safe fix
9 9 |
10 10 | print(oct(1337)[2:]) # FURB116
11 11 | print(hex(1337)[2:]) # FURB116
12 |-print(bin(1337)[2:]) # FURB116
12 |+print(f"{1337:b}") # FURB116
13 13 |
14 14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
15 15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
12 12 |
13 13 | print(oct(1337)[2:]) # FURB116
14 14 | print(hex(1337)[2:]) # FURB116
15 |-print(bin(1337)[2:]) # FURB116
15 |+print(f"{1337:b}") # FURB116
16 16 | print(bin(+1337)[2:]) # FURB116
17 17 |
18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
FURB116.py:14:7: FURB116 Replace `bin` call with f-string
FURB116.py:16:7: FURB116 [*] Replace `bin` call with `f"{+1337:b}"`
|
12 | print(bin(1337)[2:]) # FURB116
13 |
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
16 | print(bin(+1337)[2:]) # FURB116
| ^^^^^^^^^^^^^^ FURB116
17 |
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
= help: Replace with `f"{+1337:b}"`
Safe fix
13 13 | print(oct(1337)[2:]) # FURB116
14 14 | print(hex(1337)[2:]) # FURB116
15 15 | print(bin(1337)[2:]) # FURB116
16 |-print(bin(+1337)[2:]) # FURB116
16 |+print(f"{+1337:b}") # FURB116
17 17 |
18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
19 19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
FURB116.py:18:7: FURB116 Replace `bin` call with f-string
|
16 | print(bin(+1337)[2:]) # FURB116
17 |
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
| ^^^^^^^^^^^^^^^^^^^^^ FURB116
15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
= help: Replace with f-string
FURB116.py:15:7: FURB116 Replace `bin` call with f-string
FURB116.py:19:7: FURB116 Replace `bin` call with f-string
|
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
| ^^^^^^^^^^^^^^^^^^^^^^ FURB116
16 |
17 | ## invalid
20 |
21 | ## invalid
|
= help: Replace with f-string
FURB116.py:32:7: FURB116 Replace `bin` call with `f"{d:b}"`
|
30 | d = datetime.datetime.now(tz=datetime.UTC)
31 | # autofix is display-only
32 | print(bin(d)[2:])
| ^^^^^^^^^^ FURB116
33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
34 | print(bin(len("xyz").numerator)[2:])
|
= help: Replace with `f"{d:b}"`
Display-only fix
29 29 |
30 30 | d = datetime.datetime.now(tz=datetime.UTC)
31 31 | # autofix is display-only
32 |-print(bin(d)[2:])
32 |+print(f"{d:b}")
33 33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
34 34 | print(bin(len("xyz").numerator)[2:])
35 35 |
FURB116.py:34:7: FURB116 Replace `bin` call with `f"{len("xyz").numerator:b}"`
|
32 | print(bin(d)[2:])
33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
34 | print(bin(len("xyz").numerator)[2:])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
35 |
36 | # autofix is display-only
|
= help: Replace with `f"{len("xyz").numerator:b}"`
Display-only fix
31 31 | # autofix is display-only
32 32 | print(bin(d)[2:])
33 33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
34 |-print(bin(len("xyz").numerator)[2:])
34 |+print(f"{len("xyz").numerator:b}")
35 35 |
36 36 | # autofix is display-only
37 37 | print(bin({0: 1}[0].numerator)[2:])
FURB116.py:37:7: FURB116 Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"`
|
36 | # autofix is display-only
37 | print(bin({0: 1}[0].numerator)[2:])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 | print(bin(ord("\\").numerator)[2:])
|
= help: Replace with `f"{ {0: 1}[0].numerator:b}"`
Display-only fix
34 34 | print(bin(len("xyz").numerator)[2:])
35 35 |
36 36 | # autofix is display-only
37 |-print(bin({0: 1}[0].numerator)[2:])
37 |+print(f"{ {0: 1}[0].numerator:b}")
38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 39 | print(bin(ord("\\").numerator)[2:])
40 40 | print(hex(sys
FURB116.py:39:7: FURB116 Replace `bin` call with `f"{ord("\\").numerator:b}"`
|
37 | print(bin({0: 1}[0].numerator)[2:])
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 | print(bin(ord("\\").numerator)[2:])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
40 | print(hex(sys
41 | .maxunicode)[2:])
|
= help: Replace with `f"{ord("\\").numerator:b}"`
Display-only fix
36 36 | # autofix is display-only
37 37 | print(bin({0: 1}[0].numerator)[2:])
38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 |-print(bin(ord("\\").numerator)[2:])
39 |+print(f"{ord("\\").numerator:b}")
40 40 | print(hex(sys
41 41 | .maxunicode)[2:])
42 42 |
FURB116.py:40:7: FURB116 Replace `hex` call with f-string
|
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 | print(bin(ord("\\").numerator)[2:])
40 | print(hex(sys
| _______^
41 | | .maxunicode)[2:])
| |________________^ FURB116
42 |
43 | # for negatives numbers autofix is display-only
|
= help: Replace with f-string
Display-only fix
37 37 | print(bin({0: 1}[0].numerator)[2:])
38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 39 | print(bin(ord("\\").numerator)[2:])
40 |-print(hex(sys
41 |-.maxunicode)[2:])
40 |+print(f"{sys
41 |+.maxunicode:x}")
42 42 |
43 43 | # for negatives numbers autofix is display-only
44 44 | print(bin(-1)[2:])
FURB116.py:44:7: FURB116 Replace `bin` call with `f"{-1:b}"`
|
43 | # for negatives numbers autofix is display-only
44 | print(bin(-1)[2:])
| ^^^^^^^^^^^ FURB116
|
= help: Replace with `f"{-1:b}"`
Display-only fix
41 41 | .maxunicode)[2:])
42 42 |
43 43 | # for negatives numbers autofix is display-only
44 |-print(bin(-1)[2:])
44 |+print(f"{-1:b}")

View File

@@ -0,0 +1,256 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB116.py:9:7: FURB116 Replace `oct` call with `f"{num:o}"`
|
7 | return num
8 |
9 | print(oct(num)[2:]) # FURB116
| ^^^^^^^^^^^^ FURB116
10 | print(hex(num)[2:]) # FURB116
11 | print(bin(num)[2:]) # FURB116
|
= help: Replace with `f"{num:o}"`
Display-only fix
6 6 | def return_num() -> int:
7 7 | return num
8 8 |
9 |-print(oct(num)[2:]) # FURB116
9 |+print(f"{num:o}") # FURB116
10 10 | print(hex(num)[2:]) # FURB116
11 11 | print(bin(num)[2:]) # FURB116
12 12 |
FURB116.py:10:7: FURB116 Replace `hex` call with `f"{num:x}"`
|
9 | print(oct(num)[2:]) # FURB116
10 | print(hex(num)[2:]) # FURB116
| ^^^^^^^^^^^^ FURB116
11 | print(bin(num)[2:]) # FURB116
|
= help: Replace with `f"{num:x}"`
Display-only fix
7 7 | return num
8 8 |
9 9 | print(oct(num)[2:]) # FURB116
10 |-print(hex(num)[2:]) # FURB116
10 |+print(f"{num:x}") # FURB116
11 11 | print(bin(num)[2:]) # FURB116
12 12 |
13 13 | print(oct(1337)[2:]) # FURB116
FURB116.py:11:7: FURB116 Replace `bin` call with `f"{num:b}"`
|
9 | print(oct(num)[2:]) # FURB116
10 | print(hex(num)[2:]) # FURB116
11 | print(bin(num)[2:]) # FURB116
| ^^^^^^^^^^^^ FURB116
12 |
13 | print(oct(1337)[2:]) # FURB116
|
= help: Replace with `f"{num:b}"`
Display-only fix
8 8 |
9 9 | print(oct(num)[2:]) # FURB116
10 10 | print(hex(num)[2:]) # FURB116
11 |-print(bin(num)[2:]) # FURB116
11 |+print(f"{num:b}") # FURB116
12 12 |
13 13 | print(oct(1337)[2:]) # FURB116
14 14 | print(hex(1337)[2:]) # FURB116
FURB116.py:13:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
|
11 | print(bin(num)[2:]) # FURB116
12 |
13 | print(oct(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:o}"`
Safe fix
10 10 | print(hex(num)[2:]) # FURB116
11 11 | print(bin(num)[2:]) # FURB116
12 12 |
13 |-print(oct(1337)[2:]) # FURB116
13 |+print(f"{1337:o}") # FURB116
14 14 | print(hex(1337)[2:]) # FURB116
15 15 | print(bin(1337)[2:]) # FURB116
16 16 | print(bin(+1337)[2:]) # FURB116
FURB116.py:14:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
|
13 | print(oct(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
15 | print(bin(1337)[2:]) # FURB116
16 | print(bin(+1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:x}"`
Safe fix
11 11 | print(bin(num)[2:]) # FURB116
12 12 |
13 13 | print(oct(1337)[2:]) # FURB116
14 |-print(hex(1337)[2:]) # FURB116
14 |+print(f"{1337:x}") # FURB116
15 15 | print(bin(1337)[2:]) # FURB116
16 16 | print(bin(+1337)[2:]) # FURB116
17 17 |
FURB116.py:15:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
|
13 | print(oct(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
16 | print(bin(+1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:b}"`
Safe fix
12 12 |
13 13 | print(oct(1337)[2:]) # FURB116
14 14 | print(hex(1337)[2:]) # FURB116
15 |-print(bin(1337)[2:]) # FURB116
15 |+print(f"{1337:b}") # FURB116
16 16 | print(bin(+1337)[2:]) # FURB116
17 17 |
18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
FURB116.py:16:7: FURB116 [*] Replace `bin` call with `f"{+1337:b}"`
|
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
16 | print(bin(+1337)[2:]) # FURB116
| ^^^^^^^^^^^^^^ FURB116
17 |
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
= help: Replace with `f"{+1337:b}"`
Safe fix
13 13 | print(oct(1337)[2:]) # FURB116
14 14 | print(hex(1337)[2:]) # FURB116
15 15 | print(bin(1337)[2:]) # FURB116
16 |-print(bin(+1337)[2:]) # FURB116
16 |+print(f"{+1337:b}") # FURB116
17 17 |
18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
19 19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
FURB116.py:18:7: FURB116 Replace `bin` call with f-string
|
16 | print(bin(+1337)[2:]) # FURB116
17 |
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
| ^^^^^^^^^^^^^^^^^^^^^ FURB116
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
= help: Replace with f-string
FURB116.py:19:7: FURB116 Replace `bin` call with f-string
|
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
| ^^^^^^^^^^^^^^^^^^^^^^ FURB116
20 |
21 | ## invalid
|
= help: Replace with f-string
FURB116.py:32:7: FURB116 Replace `bin` call with `f"{d:b}"`
|
30 | d = datetime.datetime.now(tz=datetime.UTC)
31 | # autofix is display-only
32 | print(bin(d)[2:])
| ^^^^^^^^^^ FURB116
33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
34 | print(bin(len("xyz").numerator)[2:])
|
= help: Replace with `f"{d:b}"`
Display-only fix
29 29 |
30 30 | d = datetime.datetime.now(tz=datetime.UTC)
31 31 | # autofix is display-only
32 |-print(bin(d)[2:])
32 |+print(f"{d:b}")
33 33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
34 34 | print(bin(len("xyz").numerator)[2:])
35 35 |
FURB116.py:34:7: FURB116 Replace `bin` call with f-string
|
32 | print(bin(d)[2:])
33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
34 | print(bin(len("xyz").numerator)[2:])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
35 |
36 | # autofix is display-only
|
= help: Replace with f-string
FURB116.py:37:7: FURB116 Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"`
|
36 | # autofix is display-only
37 | print(bin({0: 1}[0].numerator)[2:])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 | print(bin(ord("\\").numerator)[2:])
|
= help: Replace with `f"{ {0: 1}[0].numerator:b}"`
Display-only fix
34 34 | print(bin(len("xyz").numerator)[2:])
35 35 |
36 36 | # autofix is display-only
37 |-print(bin({0: 1}[0].numerator)[2:])
37 |+print(f"{ {0: 1}[0].numerator:b}")
38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 39 | print(bin(ord("\\").numerator)[2:])
40 40 | print(hex(sys
FURB116.py:39:7: FURB116 Replace `bin` call with f-string
|
37 | print(bin({0: 1}[0].numerator)[2:])
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 | print(bin(ord("\\").numerator)[2:])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
40 | print(hex(sys
41 | .maxunicode)[2:])
|
= help: Replace with f-string
FURB116.py:40:7: FURB116 Replace `hex` call with f-string
|
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
39 | print(bin(ord("\\").numerator)[2:])
40 | print(hex(sys
| _______^
41 | | .maxunicode)[2:])
| |________________^ FURB116
42 |
43 | # for negatives numbers autofix is display-only
|
= help: Replace with f-string
FURB116.py:44:7: FURB116 Replace `bin` call with `f"{-1:b}"`
|
43 | # for negatives numbers autofix is display-only
44 | print(bin(-1)[2:])
| ^^^^^^^^^^^ FURB116
|
= help: Replace with `f"{-1:b}"`
Display-only fix
41 41 | .maxunicode)[2:])
42 42 |
43 43 | # for negatives numbers autofix is display-only
44 |-print(bin(-1)[2:])
44 |+print(f"{-1:b}")

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -48,6 +49,14 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare)
};
let semantic = checker.semantic();
if is_empty(right, semantic) {
checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range()));
}
}
fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool {
let set_methods = ["set", "frozenset"];
let collection_methods = [
"list",
"tuple",
@@ -59,7 +68,7 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare)
"str",
];
let is_empty_collection = match right {
match expr {
Expr::List(ast::ExprList { elts, .. }) => elts.is_empty(),
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.is_empty(),
Expr::Set(ast::ExprSet { elts, .. }) => elts.is_empty(),
@@ -75,15 +84,19 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare)
arguments,
range: _,
}) => {
arguments.is_empty()
&& collection_methods
if arguments.is_empty() {
collection_methods
.iter()
.any(|s| semantic.match_builtin_expr(func, s))
} else if let Some(arg) = arguments.find_positional(0) {
set_methods
.iter()
.any(|s| semantic.match_builtin_expr(func, s))
&& is_empty(arg, semantic)
} else {
false
}
}
_ => false,
};
if is_empty_collection {
checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range()));
}
}

View File

@@ -61,6 +61,12 @@ use super::helpers::{dataclass_kind, DataclassKind};
/// foo = Foo() # Prints '1 2'.
/// ```
///
/// ## Fix safety
///
/// This fix is always marked as unsafe because, although switching to `InitVar` is usually correct,
/// it is incorrect when the parameter is not intended to be part of the public API or when the value
/// is meant to be shared across all instances.
///
/// ## References
/// - [Python documentation: Post-init processing](https://docs.python.org/3/library/dataclasses.html#post-init-processing)
/// - [Python documentation: Init-only variables](https://docs.python.org/3/library/dataclasses.html#init-only-variables)

View File

@@ -29,6 +29,14 @@ use crate::{checkers::ast::Checker, importer::ImportRequest};
/// pairwise(letters) # ("A", "B"), ("B", "C"), ("C", "D")
/// ```
///
/// ## Fix safety
///
/// The fix is always marked unsafe because it assumes that slicing an object
/// (e.g., `obj[1:]`) produces a value with the same type and iteration behavior
/// as the original object, which is not guaranteed for user-defined types that
/// override `__getitem__` without properly handling slices. Moreover, the fix
/// could delete comments.
///
/// ## References
/// - [Python documentation: `itertools.pairwise`](https://docs.python.org/3/library/itertools.html#itertools.pairwise)
#[derive(ViolationMetadata)]

View File

@@ -187,6 +187,7 @@ RUF060.py:20:1: RUF060 Unnecessary membership test on empty collection
20 | b"a" in bytes()
| ^^^^^^^^^^^^^^^ RUF060
21 | 1 in frozenset()
22 | 1 in set(set())
|
RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection
@@ -195,6 +196,35 @@ RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection
20 | b"a" in bytes()
21 | 1 in frozenset()
| ^^^^^^^^^^^^^^^^ RUF060
22 |
23 | # OK
22 | 1 in set(set())
23 | 2 in frozenset([])
|
RUF060.py:22:1: RUF060 Unnecessary membership test on empty collection
|
20 | b"a" in bytes()
21 | 1 in frozenset()
22 | 1 in set(set())
| ^^^^^^^^^^^^^^^ RUF060
23 | 2 in frozenset([])
24 | '' in set("")
|
RUF060.py:23:1: RUF060 Unnecessary membership test on empty collection
|
21 | 1 in frozenset()
22 | 1 in set(set())
23 | 2 in frozenset([])
| ^^^^^^^^^^^^^^^^^^ RUF060
24 | '' in set("")
|
RUF060.py:24:1: RUF060 Unnecessary membership test on empty collection
|
22 | 1 in set(set())
23 | 2 in frozenset([])
24 | '' in set("")
| ^^^^^^^^^^^^^ RUF060
25 |
26 | # OK
|

View File

@@ -0,0 +1,8 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:2:22: SyntaxError: named expression cannot be used within a generic definition
|
2 | def f[T](x: int) -> (y := 3): return x
| ^^^^^^
|

View File

@@ -0,0 +1,9 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:2:13: SyntaxError: yield expression cannot be used within a generic definition
|
2 | class C[T]((yield from [object])):
| ^^^^^^^^^^^^^^^^^^^
3 | pass
|

View File

@@ -0,0 +1,8 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:2:11: SyntaxError: yield expression cannot be used within a type alias
|
2 | type Y = (yield 1)
| ^^^^^^^
|

View File

@@ -0,0 +1,8 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:2:12: SyntaxError: yield expression cannot be used within a TypeVar bound
|
2 | type X[T: (yield 1)] = int
| ^^^^^^^
|

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:3:10: SyntaxError: name capture `irrefutable` makes remaining patterns unreachable
|
2 | match value:
3 | case irrefutable:
| ^^^^^^^^^^^
4 | pass
5 | case 1:
|

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:3:10: SyntaxError: wildcard makes remaining patterns unreachable
|
2 | match value:
3 | case _:
| ^
4 | pass
5 | case 1:
|

View File

@@ -0,0 +1,8 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:1:1: SyntaxError: starred assignment target must be in a list or tuple
|
1 | *a = [1, 2, 3, 4]
| ^^
|

View File

@@ -0,0 +1,8 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:2:1: SyntaxError: cannot assign to `__debug__`
|
2 | __debug__ = False
| ^^^^^^^^^
|

View File

@@ -0,0 +1,9 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:2:15: SyntaxError: cannot assign to `__debug__`
|
2 | class Generic[__debug__]:
| ^^^^^^^^^
3 | pass
|

View File

@@ -0,0 +1,9 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:2:13: SyntaxError: cannot assign to `__debug__`
|
2 | def process(__debug__):
| ^^^^^^^^^
3 | pass
|

View File

@@ -2844,7 +2844,7 @@ impl Arguments {
self.find_argument(name, position).map(ArgOrKeyword::value)
}
/// Return the the argument with the given name or at the given position, or `None` if no such
/// Return the argument with the given name or at the given position, or `None` if no such
/// argument exists. Used to retrieve arguments that can be provided _either_ as keyword or
/// positional arguments.
pub fn find_argument(&self, name: &str, position: usize) -> Option<ArgOrKeyword> {

View File

@@ -590,7 +590,7 @@ pub fn walk_except_handler<'a, V: Visitor<'a> + ?Sized>(
}
pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) {
// Note that the there might be keywords before the last arg, e.g. in
// Note that there might be keywords before the last arg, e.g. in
// f(*args, a=2, *args2, **kwargs)`, but we follow Python in evaluating first `args` and then
// `keywords`. See also [Arguments::arguments_source_order`].
for arg in &*arguments.args {

View File

@@ -234,9 +234,6 @@ pub fn walk_stmt<V: Transformer + ?Sized>(visitor: &V, stmt: &mut Stmt) {
visitor.visit_expr(test);
visitor.visit_body(body);
for clause in elif_else_clauses {
if let Some(test) = &mut clause.test {
visitor.visit_expr(test);
}
walk_elif_else_clause(visitor, clause);
}
}
@@ -576,7 +573,7 @@ pub fn walk_except_handler<V: Transformer + ?Sized>(
}
pub fn walk_arguments<V: Transformer + ?Sized>(visitor: &V, arguments: &mut Arguments) {
// Note that the there might be keywords before the last arg, e.g. in
// Note that there might be keywords before the last arg, e.g. in
// f(*args, a=2, *args2, **kwargs)`, but we follow Python in evaluating first `args` and then
// `keywords`. See also [Arguments::arguments_source_order`].
for arg in &mut *arguments.args {

View File

@@ -1,5 +1,5 @@
# For tuple expression, the minimum binding power of star expression is bitwise or.
# Test the first and any other element as the there are two separate calls.
# Test the first and any other element as there are two separate calls.
(*x in y, z, *x in y)
(*not x, z, *not x)

View File

@@ -7,24 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize
```
Module(
ModModule {
range: 0..536,
range: 0..532,
body: [
Expr(
StmtExpr {
range: 161..182,
range: 157..178,
value: Tuple(
ExprTuple {
range: 161..182,
range: 157..178,
elts: [
Starred(
ExprStarred {
range: 162..169,
range: 158..165,
value: Compare(
ExprCompare {
range: 163..169,
range: 159..165,
left: Name(
ExprName {
range: 163..164,
range: 159..160,
id: Name("x"),
ctx: Load,
},
@@ -35,7 +35,7 @@ Module(
comparators: [
Name(
ExprName {
range: 168..169,
range: 164..165,
id: Name("y"),
ctx: Load,
},
@@ -48,20 +48,20 @@ Module(
),
Name(
ExprName {
range: 171..172,
range: 167..168,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 174..181,
range: 170..177,
value: Compare(
ExprCompare {
range: 175..181,
range: 171..177,
left: Name(
ExprName {
range: 175..176,
range: 171..172,
id: Name("x"),
ctx: Load,
},
@@ -72,7 +72,7 @@ Module(
comparators: [
Name(
ExprName {
range: 180..181,
range: 176..177,
id: Name("y"),
ctx: Load,
},
@@ -92,21 +92,21 @@ Module(
),
Expr(
StmtExpr {
range: 183..202,
range: 179..198,
value: Tuple(
ExprTuple {
range: 183..202,
range: 179..198,
elts: [
Starred(
ExprStarred {
range: 184..190,
range: 180..186,
value: UnaryOp(
ExprUnaryOp {
range: 185..190,
range: 181..186,
op: Not,
operand: Name(
ExprName {
range: 189..190,
range: 185..186,
id: Name("x"),
ctx: Load,
},
@@ -118,21 +118,21 @@ Module(
),
Name(
ExprName {
range: 192..193,
range: 188..189,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 195..201,
range: 191..197,
value: UnaryOp(
ExprUnaryOp {
range: 196..201,
range: 192..197,
op: Not,
operand: Name(
ExprName {
range: 200..201,
range: 196..197,
id: Name("x"),
ctx: Load,
},
@@ -151,29 +151,29 @@ Module(
),
Expr(
StmtExpr {
range: 203..226,
range: 199..222,
value: Tuple(
ExprTuple {
range: 203..226,
range: 199..222,
elts: [
Starred(
ExprStarred {
range: 204..212,
range: 200..208,
value: BoolOp(
ExprBoolOp {
range: 205..212,
range: 201..208,
op: And,
values: [
Name(
ExprName {
range: 205..206,
range: 201..202,
id: Name("x"),
ctx: Load,
},
),
Name(
ExprName {
range: 211..212,
range: 207..208,
id: Name("y"),
ctx: Load,
},
@@ -186,29 +186,29 @@ Module(
),
Name(
ExprName {
range: 214..215,
range: 210..211,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 217..225,
range: 213..221,
value: BoolOp(
ExprBoolOp {
range: 218..225,
range: 214..221,
op: And,
values: [
Name(
ExprName {
range: 218..219,
range: 214..215,
id: Name("x"),
ctx: Load,
},
),
Name(
ExprName {
range: 224..225,
range: 220..221,
id: Name("y"),
ctx: Load,
},
@@ -228,29 +228,29 @@ Module(
),
Expr(
StmtExpr {
range: 227..248,
range: 223..244,
value: Tuple(
ExprTuple {
range: 227..248,
range: 223..244,
elts: [
Starred(
ExprStarred {
range: 228..235,
range: 224..231,
value: BoolOp(
ExprBoolOp {
range: 229..235,
range: 225..231,
op: Or,
values: [
Name(
ExprName {
range: 229..230,
range: 225..226,
id: Name("x"),
ctx: Load,
},
),
Name(
ExprName {
range: 234..235,
range: 230..231,
id: Name("y"),
ctx: Load,
},
@@ -263,29 +263,29 @@ Module(
),
Name(
ExprName {
range: 237..238,
range: 233..234,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 240..247,
range: 236..243,
value: BoolOp(
ExprBoolOp {
range: 241..247,
range: 237..243,
op: Or,
values: [
Name(
ExprName {
range: 241..242,
range: 237..238,
id: Name("x"),
ctx: Load,
},
),
Name(
ExprName {
range: 246..247,
range: 242..243,
id: Name("y"),
ctx: Load,
},
@@ -305,33 +305,33 @@ Module(
),
Expr(
StmtExpr {
range: 249..290,
range: 245..286,
value: Tuple(
ExprTuple {
range: 249..290,
range: 245..286,
elts: [
Starred(
ExprStarred {
range: 250..267,
range: 246..263,
value: If(
ExprIf {
range: 251..267,
range: 247..263,
test: BooleanLiteral(
ExprBooleanLiteral {
range: 256..260,
range: 252..256,
value: true,
},
),
body: Name(
ExprName {
range: 251..252,
range: 247..248,
id: Name("x"),
ctx: Load,
},
),
orelse: Name(
ExprName {
range: 266..267,
range: 262..263,
id: Name("y"),
ctx: Load,
},
@@ -343,33 +343,33 @@ Module(
),
Name(
ExprName {
range: 269..270,
range: 265..266,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 272..289,
range: 268..285,
value: If(
ExprIf {
range: 273..289,
range: 269..285,
test: BooleanLiteral(
ExprBooleanLiteral {
range: 278..282,
range: 274..278,
value: true,
},
),
body: Name(
ExprName {
range: 273..274,
range: 269..270,
id: Name("x"),
ctx: Load,
},
),
orelse: Name(
ExprName {
range: 288..289,
range: 284..285,
id: Name("y"),
ctx: Load,
},
@@ -388,29 +388,29 @@ Module(
),
Expr(
StmtExpr {
range: 291..322,
range: 287..318,
value: Tuple(
ExprTuple {
range: 291..322,
range: 287..318,
elts: [
Starred(
ExprStarred {
range: 292..304,
range: 288..300,
value: Lambda(
ExprLambda {
range: 293..304,
range: 289..300,
parameters: Some(
Parameters {
range: 300..301,
range: 296..297,
posonlyargs: [],
args: [
ParameterWithDefault {
range: 300..301,
range: 296..297,
parameter: Parameter {
range: 300..301,
range: 296..297,
name: Identifier {
id: Name("x"),
range: 300..301,
range: 296..297,
},
annotation: None,
},
@@ -424,7 +424,7 @@ Module(
),
body: Name(
ExprName {
range: 303..304,
range: 299..300,
id: Name("x"),
ctx: Load,
},
@@ -436,29 +436,29 @@ Module(
),
Name(
ExprName {
range: 306..307,
range: 302..303,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 309..321,
range: 305..317,
value: Lambda(
ExprLambda {
range: 310..321,
range: 306..317,
parameters: Some(
Parameters {
range: 317..318,
range: 313..314,
posonlyargs: [],
args: [
ParameterWithDefault {
range: 317..318,
range: 313..314,
parameter: Parameter {
range: 317..318,
range: 313..314,
name: Identifier {
id: Name("x"),
range: 317..318,
range: 313..314,
},
annotation: None,
},
@@ -472,7 +472,7 @@ Module(
),
body: Name(
ExprName {
range: 320..321,
range: 316..317,
id: Name("x"),
ctx: Load,
},
@@ -491,20 +491,20 @@ Module(
),
Expr(
StmtExpr {
range: 323..344,
range: 319..340,
value: Tuple(
ExprTuple {
range: 323..344,
range: 319..340,
elts: [
Named(
ExprNamed {
range: 324..331,
range: 320..327,
target: Starred(
ExprStarred {
range: 324..326,
range: 320..322,
value: Name(
ExprName {
range: 325..326,
range: 321..322,
id: Name("x"),
ctx: Store,
},
@@ -514,7 +514,7 @@ Module(
),
value: NumberLiteral(
ExprNumberLiteral {
range: 330..331,
range: 326..327,
value: Int(
2,
),
@@ -524,20 +524,20 @@ Module(
),
Name(
ExprName {
range: 333..334,
range: 329..330,
id: Name("z"),
ctx: Load,
},
),
Named(
ExprNamed {
range: 336..343,
range: 332..339,
target: Starred(
ExprStarred {
range: 336..338,
range: 332..334,
value: Name(
ExprName {
range: 337..338,
range: 333..334,
id: Name("x"),
ctx: Store,
},
@@ -547,7 +547,7 @@ Module(
),
value: NumberLiteral(
ExprNumberLiteral {
range: 342..343,
range: 338..339,
value: Int(
2,
),
@@ -564,20 +564,20 @@ Module(
),
Expr(
StmtExpr {
range: 367..386,
range: 363..382,
value: Tuple(
ExprTuple {
range: 367..386,
range: 363..382,
elts: [
Starred(
ExprStarred {
range: 367..374,
range: 363..370,
value: Compare(
ExprCompare {
range: 368..374,
range: 364..370,
left: Name(
ExprName {
range: 368..369,
range: 364..365,
id: Name("x"),
ctx: Load,
},
@@ -588,7 +588,7 @@ Module(
comparators: [
Name(
ExprName {
range: 373..374,
range: 369..370,
id: Name("y"),
ctx: Load,
},
@@ -601,20 +601,20 @@ Module(
),
Name(
ExprName {
range: 376..377,
range: 372..373,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 379..386,
range: 375..382,
value: Compare(
ExprCompare {
range: 380..386,
range: 376..382,
left: Name(
ExprName {
range: 380..381,
range: 376..377,
id: Name("x"),
ctx: Load,
},
@@ -625,7 +625,7 @@ Module(
comparators: [
Name(
ExprName {
range: 385..386,
range: 381..382,
id: Name("y"),
ctx: Load,
},
@@ -645,21 +645,21 @@ Module(
),
Expr(
StmtExpr {
range: 387..404,
range: 383..400,
value: Tuple(
ExprTuple {
range: 387..404,
range: 383..400,
elts: [
Starred(
ExprStarred {
range: 387..393,
range: 383..389,
value: UnaryOp(
ExprUnaryOp {
range: 388..393,
range: 384..389,
op: Not,
operand: Name(
ExprName {
range: 392..393,
range: 388..389,
id: Name("x"),
ctx: Load,
},
@@ -671,21 +671,21 @@ Module(
),
Name(
ExprName {
range: 395..396,
range: 391..392,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 398..404,
range: 394..400,
value: UnaryOp(
ExprUnaryOp {
range: 399..404,
range: 395..400,
op: Not,
operand: Name(
ExprName {
range: 403..404,
range: 399..400,
id: Name("x"),
ctx: Load,
},
@@ -704,29 +704,29 @@ Module(
),
Expr(
StmtExpr {
range: 405..426,
range: 401..422,
value: Tuple(
ExprTuple {
range: 405..426,
range: 401..422,
elts: [
Starred(
ExprStarred {
range: 405..413,
range: 401..409,
value: BoolOp(
ExprBoolOp {
range: 406..413,
range: 402..409,
op: And,
values: [
Name(
ExprName {
range: 406..407,
range: 402..403,
id: Name("x"),
ctx: Load,
},
),
Name(
ExprName {
range: 412..413,
range: 408..409,
id: Name("y"),
ctx: Load,
},
@@ -739,29 +739,29 @@ Module(
),
Name(
ExprName {
range: 415..416,
range: 411..412,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 418..426,
range: 414..422,
value: BoolOp(
ExprBoolOp {
range: 419..426,
range: 415..422,
op: And,
values: [
Name(
ExprName {
range: 419..420,
range: 415..416,
id: Name("x"),
ctx: Load,
},
),
Name(
ExprName {
range: 425..426,
range: 421..422,
id: Name("y"),
ctx: Load,
},
@@ -781,29 +781,29 @@ Module(
),
Expr(
StmtExpr {
range: 427..446,
range: 423..442,
value: Tuple(
ExprTuple {
range: 427..446,
range: 423..442,
elts: [
Starred(
ExprStarred {
range: 427..434,
range: 423..430,
value: BoolOp(
ExprBoolOp {
range: 428..434,
range: 424..430,
op: Or,
values: [
Name(
ExprName {
range: 428..429,
range: 424..425,
id: Name("x"),
ctx: Load,
},
),
Name(
ExprName {
range: 433..434,
range: 429..430,
id: Name("y"),
ctx: Load,
},
@@ -816,29 +816,29 @@ Module(
),
Name(
ExprName {
range: 436..437,
range: 432..433,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 439..446,
range: 435..442,
value: BoolOp(
ExprBoolOp {
range: 440..446,
range: 436..442,
op: Or,
values: [
Name(
ExprName {
range: 440..441,
range: 436..437,
id: Name("x"),
ctx: Load,
},
),
Name(
ExprName {
range: 445..446,
range: 441..442,
id: Name("y"),
ctx: Load,
},
@@ -858,33 +858,33 @@ Module(
),
Expr(
StmtExpr {
range: 447..486,
range: 443..482,
value: Tuple(
ExprTuple {
range: 447..486,
range: 443..482,
elts: [
Starred(
ExprStarred {
range: 447..464,
range: 443..460,
value: If(
ExprIf {
range: 448..464,
range: 444..460,
test: BooleanLiteral(
ExprBooleanLiteral {
range: 453..457,
range: 449..453,
value: true,
},
),
body: Name(
ExprName {
range: 448..449,
range: 444..445,
id: Name("x"),
ctx: Load,
},
),
orelse: Name(
ExprName {
range: 463..464,
range: 459..460,
id: Name("y"),
ctx: Load,
},
@@ -896,33 +896,33 @@ Module(
),
Name(
ExprName {
range: 466..467,
range: 462..463,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 469..486,
range: 465..482,
value: If(
ExprIf {
range: 470..486,
range: 466..482,
test: BooleanLiteral(
ExprBooleanLiteral {
range: 475..479,
range: 471..475,
value: true,
},
),
body: Name(
ExprName {
range: 470..471,
range: 466..467,
id: Name("x"),
ctx: Load,
},
),
orelse: Name(
ExprName {
range: 485..486,
range: 481..482,
id: Name("y"),
ctx: Load,
},
@@ -941,29 +941,29 @@ Module(
),
Expr(
StmtExpr {
range: 487..516,
range: 483..512,
value: Tuple(
ExprTuple {
range: 487..516,
range: 483..512,
elts: [
Starred(
ExprStarred {
range: 487..499,
range: 483..495,
value: Lambda(
ExprLambda {
range: 488..499,
range: 484..495,
parameters: Some(
Parameters {
range: 495..496,
range: 491..492,
posonlyargs: [],
args: [
ParameterWithDefault {
range: 495..496,
range: 491..492,
parameter: Parameter {
range: 495..496,
range: 491..492,
name: Identifier {
id: Name("x"),
range: 495..496,
range: 491..492,
},
annotation: None,
},
@@ -977,7 +977,7 @@ Module(
),
body: Name(
ExprName {
range: 498..499,
range: 494..495,
id: Name("x"),
ctx: Load,
},
@@ -989,29 +989,29 @@ Module(
),
Name(
ExprName {
range: 501..502,
range: 497..498,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 504..516,
range: 500..512,
value: Lambda(
ExprLambda {
range: 505..516,
range: 501..512,
parameters: Some(
Parameters {
range: 512..513,
range: 508..509,
posonlyargs: [],
args: [
ParameterWithDefault {
range: 512..513,
range: 508..509,
parameter: Parameter {
range: 512..513,
range: 508..509,
name: Identifier {
id: Name("x"),
range: 512..513,
range: 508..509,
},
annotation: None,
},
@@ -1025,7 +1025,7 @@ Module(
),
body: Name(
ExprName {
range: 515..516,
range: 511..512,
id: Name("x"),
ctx: Load,
},
@@ -1044,13 +1044,13 @@ Module(
),
Expr(
StmtExpr {
range: 517..519,
range: 513..515,
value: Starred(
ExprStarred {
range: 517..519,
range: 513..515,
value: Name(
ExprName {
range: 518..519,
range: 514..515,
id: Name("x"),
ctx: Load,
},
@@ -1062,14 +1062,14 @@ Module(
),
Expr(
StmtExpr {
range: 523..536,
range: 519..532,
value: Tuple(
ExprTuple {
range: 523..536,
range: 519..532,
elts: [
NumberLiteral(
ExprNumberLiteral {
range: 523..524,
range: 519..520,
value: Int(
2,
),
@@ -1077,17 +1077,17 @@ Module(
),
Name(
ExprName {
range: 526..527,
range: 522..523,
id: Name("z"),
ctx: Load,
},
),
Starred(
ExprStarred {
range: 529..531,
range: 525..527,
value: Name(
ExprName {
range: 530..531,
range: 526..527,
id: Name("x"),
ctx: Load,
},
@@ -1097,7 +1097,7 @@ Module(
),
NumberLiteral(
ExprNumberLiteral {
range: 535..536,
range: 531..532,
value: Int(
2,
),
@@ -1117,7 +1117,7 @@ Module(
## Errors
|
2 | # Test the first and any other element as the there are two separate calls.
2 | # Test the first and any other element as there are two separate calls.
3 |
4 | (*x in y, z, *x in y)
| ^^^^^^ Syntax Error: Comparison expression cannot be used here
@@ -1127,7 +1127,7 @@ Module(
|
2 | # Test the first and any other element as the there are two separate calls.
2 | # Test the first and any other element as there are two separate calls.
3 |
4 | (*x in y, z, *x in y)
| ^^^^^^ Syntax Error: Comparison expression cannot be used here

View File

@@ -23,7 +23,6 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
| "_collections"
| "_collections_abc"
| "_compat_pickle"
| "_compression"
| "_contextvars"
| "_csv"
| "_ctypes"
@@ -285,6 +284,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
) | (
7,
"_bootlocale"
| "_compression"
| "_crypt"
| "_dummy_thread"
| "_msi"
@@ -324,6 +324,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
) | (
8,
"_bootlocale"
| "_compression"
| "_crypt"
| "_dummy_thread"
| "_msi"
@@ -368,6 +369,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
"_aix_support"
| "_bootlocale"
| "_bootsubprocess"
| "_compression"
| "_crypt"
| "_msi"
| "_peg_parser"
@@ -399,7 +401,6 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
| "nntplib"
| "ossaudiodev"
| "parser"
| "peg_parser"
| "pipes"
| "smtpd"
| "sndhdr"
@@ -414,6 +415,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
10,
"_aix_support"
| "_bootsubprocess"
| "_compression"
| "_crypt"
| "_msi"
| "_posixshmem"
@@ -460,6 +462,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
| "__phello_alias__"
| "_aix_support"
| "_bootsubprocess"
| "_compression"
| "_crypt"
| "_msi"
| "_posixshmem"
@@ -507,6 +510,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
| "__hello_only__"
| "__phello_alias__"
| "_aix_support"
| "_compression"
| "_crypt"
| "_msi"
| "_posixshmem"
@@ -554,7 +558,9 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
| "__phello_alias__"
| "_aix_support"
| "_android_support"
| "_apple_support"
| "_colorize"
| "_compression"
| "_interpchannels"
| "_interpqueues"
| "_interpreters"
@@ -583,6 +589,50 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
| "tomllib"
| "xxlimited_35"
| "zoneinfo"
) | (
14,
"__hello_alias__"
| "__hello_only__"
| "__phello_alias__"
| "_aix_support"
| "_android_support"
| "_apple_support"
| "_ast_unparse"
| "_colorize"
| "_hmac"
| "_interpchannels"
| "_interpqueues"
| "_interpreters"
| "_ios_support"
| "_opcode_metadata"
| "_posixshmem"
| "_py_warnings"
| "_pydatetime"
| "_pylong"
| "_pyrepl"
| "_remote_debugging"
| "_sha2"
| "_statistics"
| "_suggestions"
| "_sysconfig"
| "_testcapi_datetime"
| "_testclinic"
| "_testclinic_limited"
| "_testinternalcapi"
| "_testlimitedcapi"
| "_testsinglephase"
| "_tokenize"
| "_types"
| "_typing"
| "_wmi"
| "_zoneinfo"
| "_zstd"
| "annotationlib"
| "compression"
| "graphlib"
| "tomllib"
| "xxlimited_35"
| "zoneinfo"
)
)
}

141
crates/ty/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,141 @@
# Contributing to ty
Welcome! We're happy to have you here. Thank you in advance for your contribution to ty.
> [!NOTE]
>
> This guide is for ty. If you're looking to contribute to Ruff, please see
> [the Ruff contributing guide](../../CONTRIBUTING.md).
## The Basics
We welcome contributions in the form of pull requests.
For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g. new diagnostics, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/astral-sh/ty/issues) outlining your proposed change.
You can also join us on [Discord](https://discord.com/invite/astral-sh) to discuss your idea with the
community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ty/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
in the issue tracker, along with [bugs](https://github.com/astral-sh/ty/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
that are ready for contributions.
### Prerequisites
ty is written in Rust. You'll need to install the
[Rust toolchain](https://www.rust-lang.org/tools/install) for development.
You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to
run Python utility commands.
You can optionally install pre-commit hooks to automatically run the validation checks
when making a commit:
```shell
uv tool install pre-commit
pre-commit install
```
We recommend [nextest](https://nexte.st/) to run ty's test suite (via `cargo nextest run`),
though it's not strictly necessary:
```shell
cargo install cargo-nextest --locked
```
Throughout this guide, any usages of `cargo test` can be replaced with `cargo nextest run`,
if you choose to install `nextest`.
### Development
After cloning the repository, run ty locally from the repository root with:
```shell
cargo run --bin ty -- check --project /path/to/project/
```
Prior to opening a pull request, ensure that your code has been auto-formatted,
and that it passes both the lint and test validation checks:
```shell
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
cargo test # Rust testing
uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
```
These checks will run on GitHub Actions when you open your pull request, but running them locally
will save you time and expedite the merge process.
If you're using VS Code, you can also install the recommended [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) extension to get these checks while editing.
Include the text `[ty]` at the beginning of your pull request title, to distinguish ty pull requests
from Ruff ones.
Your pull request will be reviewed by a maintainer, which may involve a few rounds of iteration
prior to merging.
### Debugging ty
ty can optionally emit extensive tracing output, which can be very useful in understanding its
operation and debugging issues; see [`crates/ty/docs/tracing.md`](./docs/tracing.md) for details.
### Project Structure
The codebase is structured as a monorepo with a [flat crate structure](https://matklad.github.io/2021/08/22/large-rust-workspaces.html),
such that all crates are contained in a flat `crates` directory.
The vast majority of ty's code lives in the `ty_python_semantic` crate (located at
`crates/ty_python_semantic`). As a contributor, that's the crate that'll probably be most relevant
to you.
At the time of writing, the repository includes the following ty-specific crates (in addition to
crates shared with Ruff, such as `ruff_db`, `ruff_python_ast`, and `ruff_python_parser`):
- `ty_python_semantic`: The core type checker, which includes the type inference engine and
semantic analysis.
- `ty_test`: The Markdown-based test framework for ty, "mdtest".
- `ty`: The command-line interface.
- `ty_ide`: IDE features (hover, go-to-definition, autocomplete) for the language server.
- `ty_project`: Discovery and representation of a Python project to be checked by ty.
- `ty_server`: The ty language server.
- `ty_vendored`: A vendored copy of [typeshed](https://github.com/python/typeshed), which holds type
annotations for the Python standard library.
- `ty_wasm`: library crate for exposing ty as a WebAssembly module. Powers the
[ty Playground](https://play.ty.dev/).
## Writing tests
Core type checking tests are written as Markdown code blocks.
They can be found in [`crates/ty_python_semantic/resources/mdtest`][resources-mdtest].
See [`crates/ty_test/README.md`][mdtest-readme] for more information
on the test framework itself.
Any ty pull request to improve ty's type inference or type checking logic should include mdtests
demonstrating the effect of the change.
We write mdtests in a "literate" style, with prose explaining the motivation of each test, and any
context necessary to understand the feature being demonstrated.
### Property tests
ty uses property-based testing to test the core type relations. These tests are located in
[`crates/ty_python_semantic/src/types/property_tests.rs`](../ty_python_semantic/src/types/property_tests.rs).
The property tests do not run in CI on every PR, just once daily. It is advisable to run them
locally after modifying core type relation methods (`is_subtype_of`, `is_equivalent_to`, etc.) to
ensure that the changes do not break any of the properties.
## Ecosystem CI (mypy-primer)
GitHub Actions will run your changes against a number of real-world projects from GitHub and
report on any linter or formatter differences. See [`crates/ty/docs/mypy_primer.md`](./docs/mypy_primer.md)
for instructions on running these checks locally.
## Coding guidelines
We use the [Salsa](https:://github.com/salsa-rs/salsa) library for incremental computation. Many
methods take a Salsa database (usually `db: &'db dyn Db`) as an argument. This should always be the
first argument (or second after `self`).
[mdtest-readme]: ../ty_test/README.md
[resources-mdtest]: ../ty_python_semantic/resources/mdtest

View File

@@ -28,6 +28,7 @@ colored = { workspace = true }
countme = { workspace = true, features = ["enable"] }
crossbeam = { workspace = true }
ctrlc = { version = "3.4.4" }
indicatif = { workspace = true }
jiff = { workspace = true }
rayon = { workspace = true }
salsa = { workspace = true }

View File

@@ -1,25 +1,9 @@
# ty
ty is an extremely fast type checker.
Currently, it is a work-in-progress and not ready for user testing.
Currently, it is a work-in-progress and not ready for production use.
ty is designed to prioritize good type inference, even in unannotated code,
and aims to avoid false positives.
The Rust code for ty lives in this repository; see [CONTRIBUTING.md](CONTRIBUTING.md) for more
information on contributing to ty.
While ty will produce similar results to mypy and pyright on many codebases,
100% compatibility with these tools is a non-goal.
On some codebases, ty's design decisions lead to different outcomes
than you would get from running one of these more established tools.
## Contributing
Core type checking tests are written as Markdown code blocks.
They can be found in [`ty_python_semantic/resources/mdtest`][resources-mdtest].
See [`ty_test/README.md`][mdtest-readme] for more information
on the test framework itself.
The list of open issues can be found [here][open-issues].
[mdtest-readme]: ../ty_test/README.md
[open-issues]: https://github.com/astral-sh/ty/issues
[resources-mdtest]: ../ty_python_semantic/resources/mdtest
See [the ty repo](https://github.com/astral-sh/ty) for ty documentation and releases.

View File

@@ -30,7 +30,7 @@ ty check [OPTIONS] [PATH]...
<h3 class="cli-reference">Arguments</h3>
<dl class="cli-reference"><dt id="ty-check--paths"><a href="#ty-check--paths"<code>PATHS</code></a></dt><dd><p>List of files or directories to check [default: the project root]</p>
<dl class="cli-reference"><dt id="ty-check--paths"><a href="#ty-check--paths"><code>PATHS</code></a></dt><dd><p>List of files or directories to check [default: the project root]</p>
</dd></dl>
<h3 class="cli-reference">Options</h3>
@@ -56,13 +56,17 @@ ty check [OPTIONS] [PATH]...
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
</dd><dt id="ty-check--python"><a href="#ty-check--python"><code>--python</code></a> <i>path</i></dt><dd><p>Path to the Python installation from which ty resolves type information and third-party dependencies.</p>
<p>If not specified, ty will look at the <code>VIRTUAL_ENV</code> environment variable.</p>
<p>ty will search in the path's <code>site-packages</code> directories for type information and third-party imports.</p>
<p>This option is commonly used to specify the path to a virtual environment.</p>
</dd><dt id="ty-check--python"><a href="#ty-check--python"><code>--python</code></a> <i>path</i></dt><dd><p>Path to the Python environment.</p>
<p>ty uses the Python environment to resolve type information and third-party dependencies.</p>
<p>If not specified, ty will attempt to infer it from the <code>VIRTUAL_ENV</code> environment variable or discover a <code>.venv</code> directory in the project root or working directory.</p>
<p>If a path to a Python interpreter is provided, e.g., <code>.venv/bin/python3</code>, ty will attempt to find an environment two directories up from the interpreter's path, e.g., <code>.venv</code>. At this time, ty does not invoke the interpreter to determine the location of the environment. This means that ty will not resolve dynamic executables such as a shim.</p>
<p>ty will search in the resolved environments's <code>site-packages</code> directories for type information and third-party imports.</p>
</dd><dt id="ty-check--python-platform"><a href="#ty-check--python-platform"><code>--python-platform</code></a>, <code>--platform</code> <i>platform</i></dt><dd><p>Target platform to assume when resolving types.</p>
<p>This is used to specialize the type of <code>sys.platform</code> and will affect the visibility of platform-specific functions and attributes. If the value is set to <code>all</code>, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.</p>
</dd><dt id="ty-check--python-version"><a href="#ty-check--python-version"><code>--python-version</code></a>, <code>--target-version</code> <i>version</i></dt><dd><p>Python version to assume when resolving types</p>
</dd><dt id="ty-check--python-version"><a href="#ty-check--python-version"><code>--python-version</code></a>, <code>--target-version</code> <i>version</i></dt><dd><p>Python version to assume when resolving types.</p>
<p>The Python version affects allowed syntax, type definitions of the standard library, and type definitions of first- and third-party modules that are conditional on the Python version.</p>
<p>By default, the Python version is inferred as the lower bound of the project's <code>requires-python</code> field from the <code>pyproject.toml</code>, if available. Otherwise, the latest stable version supported by ty is used, which is currently 3.13.</p>
<p>ty will not infer the Python version from the Python environment at this time.</p>
<p>Possible values:</p>
<ul>
<li><code>3.7</code></li>
@@ -121,7 +125,7 @@ ty generate-shell-completion <SHELL>
<h3 class="cli-reference">Arguments</h3>
<dl class="cli-reference"><dt id="ty-generate-shell-completion--shell"><a href="#ty-generate-shell-completion--shell"<code>SHELL</code></a></dt></dl>
<dl class="cli-reference"><dt id="ty-generate-shell-completion--shell"><a href="#ty-generate-shell-completion--shell"><code>SHELL</code></a></dt></dl>
<h3 class="cli-reference">Options</h3>

View File

@@ -1,5 +1,5 @@
# Configuration
#### [`respect-ignore-files`]
#### `respect-ignore-files`
Whether to automatically exclude files that are ignored by `.ignore`,
`.gitignore`, `.git/info/exclude`, and global `gitignore` files.
@@ -18,11 +18,11 @@ respect-ignore-files = false
---
#### [`rules`]
#### `rules`
Configures the enabled rules and their severity.
See [the rules documentation](https://github.com/astral-sh/ruff/blob/main/crates/ty/docs/rules.md) for a list of all available rules.
See [the rules documentation](https://ty.dev/rules) for a list of all available rules.
Valid severities are:
@@ -47,7 +47,7 @@ division-by-zero = "ignore"
## `environment`
#### [`extra-paths`]
#### `extra-paths`
List of user-provided paths that should take first priority in the module resolution.
Examples in other type checkers are mypy's `MYPYPATH` environment variable,
@@ -66,7 +66,7 @@ extra-paths = ["~/shared/my-search-path"]
---
#### [`python`]
#### `python`
Path to the Python installation from which ty resolves type information and third-party dependencies.
@@ -88,7 +88,7 @@ python = "./.venv"
---
#### [`python-platform`]
#### `python-platform`
Specifies the target platform that will be used to analyze the source code.
If specified, ty will understand conditions based on comparisons with `sys.platform`, such
@@ -115,7 +115,7 @@ python-platform = "win32"
---
#### [`python-version`]
#### `python-version`
Specifies the version of Python that will be used to analyze the source code.
The version should be specified as a string in the format `M.m` where `M` is the major version
@@ -139,7 +139,7 @@ python-version = "3.12"
---
#### [`typeshed`]
#### `typeshed`
Optional path to a "typeshed" directory on disk for us to use for standard-library types.
If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
@@ -160,26 +160,26 @@ typeshed = "/path/to/custom/typeshed"
## `src`
#### [`root`]
#### `root`
The root(s) of the project, used for finding first-party modules.
The root of the project, used for finding first-party modules.
**Default value**: `[".", "./src"]`
**Type**: `list[str]`
**Type**: `str`
**Example usage** (`pyproject.toml`):
```toml
[tool.ty.src]
root = ["./app"]
root = "./app"
```
---
## `terminal`
#### [`error-on-warning`]
#### `error-on-warning`
Use exit code 1 if there are any warning-level diagnostics.
@@ -199,7 +199,7 @@ error-on-warning = true
---
#### [`output-format`]
#### `output-format`
The format to use for printing diagnostic messages.

View File

@@ -50,7 +50,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L84)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L85)
</details>
## `conflicting-argument-forms`
@@ -61,11 +61,27 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<summary>detects when an argument is used as both a value and a type form in a call</summary>
### What it does
Checks whether an argument is used as both a value and a type form in a call
Checks whether an argument is used as both a value and a type form in a call.
### Why is this bad?
Such calls have confusing semantics and often indicate a logic error.
### Examples
```python
from typing import reveal_type
from ty_extensions import is_fully_static
if flag:
f = repr # Expects a value
else:
f = is_fully_static # Expects a type form
f(int) # error
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L115)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L116)
</details>
## `conflicting-declarations`
@@ -83,9 +99,19 @@ A variable with two conflicting declarations likely indicates a mistake.
Moreover, it could lead to incorrect or ill-defined type inference for
other code that relies on these variables.
### Examples
```python
if b:
a: int
else:
a: str
a = 1
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L125)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L142)
</details>
## `conflicting-metaclass`
@@ -95,11 +121,28 @@ other code that relies on these variables.
<details>
<summary>detects conflicting metaclasses</summary>
TODO #14889
### What it does
Checks for class definitions where the metaclass of the class
being created would not be a subclass of the metaclasses of
all the class's bases.
### Why is it bad?
Such a class definition raises a `TypeError` at runtime.
### Examples
```python
class M1(type): ...
class M2(type): ...
class A(metaclass=M1): ...
class B(metaclass=M2): ...
## TypeError: metaclass conflict
class C(A, B): ...
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L140)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L167)
</details>
## `cyclic-class-definition`
@@ -110,14 +153,27 @@ TODO #14889
<summary>detects cyclic class definitions</summary>
### What it does
Checks for class definitions with a cyclic inheritance chain.
Checks for class definitions in stub files that inherit
(directly or indirectly) from themselves.
### Why is it bad?
TODO #14889
Although forward references are natively supported in stub files,
inheritance cycles are still disallowed, as it is impossible to
resolve a consistent [method resolution order] for a class that
inherits from itself.
### Examples
```python
## foo.pyi
class A(B): ...
class B(A): ...
```
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L149)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L193)
</details>
## `division-by-zero`
@@ -140,7 +196,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L162)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L219)
</details>
## `duplicate-base`
@@ -154,11 +210,19 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Checks for class definitions with duplicate bases.
### Why is this bad?
Class definitions with duplicate bases raise a `TypeError` at runtime.
Class definitions with duplicate bases raise `TypeError` at runtime.
### Examples
```python
class A: ...
## TypeError: duplicate base class
class B(A, A): ...
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L180)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L237)
</details>
## `escape-character-in-forward-annotation`
@@ -295,7 +359,7 @@ TypeError: multiple bases have instance lay-out conflict
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L193)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L258)
</details>
## `inconsistent-mro`
@@ -306,14 +370,25 @@ TypeError: multiple bases have instance lay-out conflict
<summary>detects class definitions with an inconsistent MRO</summary>
### What it does
Checks for classes with an inconsistent method resolution order (MRO).
Checks for classes with an inconsistent [method resolution order] (MRO).
### Why is this bad?
Classes with an inconsistent MRO will raise a `TypeError` at runtime.
### Examples
```python
class A: ...
class B(A): ...
## TypeError: Cannot create a consistent method resolution order
class C(A, B): ...
```
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L279)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L344)
</details>
## `index-out-of-bounds`
@@ -330,9 +405,15 @@ a container.
### Why is this bad?
Using an out of bounds index will raise an `IndexError` at runtime.
### Examples
```python
t = (0, 1, 2)
t[3] # IndexError: tuple index out of range
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L292)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L368)
</details>
## `invalid-argument-type`
@@ -358,7 +439,7 @@ func("foo") # error: [invalid-argument-type]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L306)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L388)
</details>
## `invalid-assignment`
@@ -368,11 +449,24 @@ func("foo") # error: [invalid-argument-type]
<details>
<summary>detects invalid assignments</summary>
TODO #14889
### What it does
Checks for assignments where the type of the value
is not [assignable to] the type of the assignee.
### Why is this bad?
Such assignments break the rules of the type system and
weaken a type checker's ability to accurately reason about your code.
### Examples
```python
a: int = ''
```
[assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L346)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L428)
</details>
## `invalid-attribute-access`
@@ -383,20 +477,29 @@ TODO #14889
<summary>Invalid attribute access</summary>
### What it does
Makes sure that instance attribute accesses are valid.
Checks for assignments to class variables from instances
and assignments to instance variables from its class.
### Why is this bad?
Incorrect assignments break the rules of the type system and
weaken a type checker's ability to accurately reason about your code.
### Examples
```python
class C:
var: ClassVar[int] = 1
class_var: ClassVar[int] = 1
instance_var: int
C.var = 3 # okay
C().var = 3 # error: Cannot assign to class variable
C.class_var = 3 # okay
C().class_var = 3 # error: Cannot assign to class variable
C().instance_var = 3 # okay
C.instance_var = 3 # error: Cannot assign to instance variable
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1099)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
</details>
## `invalid-base`
@@ -404,13 +507,13 @@ C().var = 3 # error: Cannot assign to class variable
**Default level**: error
<details>
<summary>detects class definitions with an invalid base</summary>
<summary>detects invalid bases in class definitions</summary>
TODO #14889
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L355)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L450)
</details>
## `invalid-context-manager`
@@ -420,11 +523,23 @@ TODO #14889
<details>
<summary>detects expressions used in with statements that don't implement the context manager protocol</summary>
TODO #14889
### What it does
Checks for expressions used in `with` statements
that do not implement the context manager protocol.
### Why is this bad?
Such a statement will raise `TypeError` at runtime.
### Examples
```python
## TypeError: 'int' object does not support the context manager protocol
with 1:
print(2)
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L364)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L459)
</details>
## `invalid-declaration`
@@ -434,11 +549,25 @@ TODO #14889
<details>
<summary>detects invalid declarations</summary>
TODO #14889
### What it does
Checks for declarations where the inferred type of an existing symbol
is not [assignable to] its post-hoc declared type.
### Why is this bad?
Such declarations break the rules of the type system and
weaken a type checker's ability to accurately reason about your code.
### Examples
```python
a = 1
a: str
```
[assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L373)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L480)
</details>
## `invalid-exception-caught`
@@ -479,7 +608,7 @@ except ZeroDivisionError:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L382)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L503)
</details>
## `invalid-generic-class`
@@ -510,7 +639,7 @@ class C[U](Generic[T]): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L418)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L539)
</details>
## `invalid-legacy-type-variable`
@@ -543,7 +672,7 @@ def f(t: TypeVar("U")): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L444)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L565)
</details>
## `invalid-metaclass`
@@ -575,7 +704,7 @@ class B(metaclass=f): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L472)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L593)
</details>
## `invalid-overload`
@@ -623,7 +752,7 @@ def foo(x: int) -> int: ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L499)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L620)
</details>
## `invalid-parameter-default`
@@ -634,14 +763,21 @@ def foo(x: int) -> int: ...
<summary>detects default values that can't be assigned to the parameter's annotated type</summary>
### What it does
Checks for default values that can't be assigned to the parameter's annotated type.
Checks for default values that can't be
assigned to the parameter's annotated type.
### Why is this bad?
TODO #14889
This breaks the rules of the type system and
weakens a type checker's ability to accurately reason about your code.
### Examples
```python
def f(a: int = ''): ...
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L542)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L663)
</details>
## `invalid-protocol`
@@ -674,7 +810,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L251)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L316)
</details>
## `invalid-raise`
@@ -722,7 +858,7 @@ def g():
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L555)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L683)
</details>
## `invalid-return-type`
@@ -746,7 +882,7 @@ def func() -> int:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L327)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L409)
</details>
## `invalid-super-argument`
@@ -790,7 +926,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)`
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L598)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L726)
</details>
## `invalid-syntax-in-forward-annotation`
@@ -825,9 +961,15 @@ code seen only by the type checker, and not at runtime. Normally this flag is im
must be assigned the value `False` at runtime; the type checker will consider its value to
be `True`. If annotated, it must be annotated as a type that can accept `bool` values.
### Examples
```python
TYPE_CHECKING: str
TYPE_CHECKING = ''
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L637)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765)
</details>
## `invalid-type-form`
@@ -838,14 +980,24 @@ be `True`. If annotated, it must be annotated as a type that can accept `bool` v
<summary>detects invalid type forms</summary>
### What it does
Checks for invalid type expressions.
Checks for expressions that are used as type expressions
but cannot validly be interpreted as such.
### Why is this bad?
TODO #14889
Such expressions cannot be understood by ty.
In some cases, they might raise errors at runtime.
### Examples
```python
from typing import Annotated
a: type[1] # `1` is not a type
b: Annotated[int] # `Annotated` expects at least two arguments
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L655)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L789)
</details>
## `invalid-type-variable-constraints`
@@ -855,11 +1007,31 @@ TODO #14889
<details>
<summary>detects invalid type variable constraints</summary>
TODO #14889
### What it does
Checks for constrained [type variables] with only one constraint.
### Why is this bad?
A constrained type variable must have at least two constraints.
### Examples
```python
from typing import TypeVar
T = TypeVar('T', str) # invalid constrained TypeVar
```
Use instead:
```python
T = TypeVar('T', str, int) # valid constrained TypeVar
## or
T = TypeVar('T', bound=str) # valid bound TypeVar
```
[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L668)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L812)
</details>
## `missing-argument`
@@ -883,7 +1055,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L677)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L841)
</details>
## `no-matching-overload`
@@ -911,7 +1083,7 @@ func("string") # error: [no-matching-overload]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L696)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L860)
</details>
## `non-subscriptable`
@@ -934,7 +1106,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L719)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L883)
</details>
## `not-iterable`
@@ -959,7 +1131,7 @@ for i in 34: # TypeError: 'int' object is not iterable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L901)
</details>
## `parameter-already-assigned`
@@ -985,7 +1157,7 @@ f(1, x=2) # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L952)
</details>
## `raw-string-type-annotation`
@@ -1028,6 +1200,11 @@ def test(): -> "int":
### What it does
Makes sure that the argument of `static_assert` is statically known to be true.
### Why is this bad?
A `static_assert` call represents an explicit request from the user
for the type checker to emit an error if the argument cannot be verified
to evaluate to `True` in a boolean context.
### Examples
```python
from ty_extensions import static_assert
@@ -1039,7 +1216,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1080)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1288)
</details>
## `subclass-of-final-class`
@@ -1067,7 +1244,7 @@ class B(A): ... # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L858)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1043)
</details>
## `too-many-positional-arguments`
@@ -1093,7 +1270,7 @@ f("foo") # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L903)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1088)
</details>
## `type-assertion-failure`
@@ -1120,7 +1297,7 @@ def _(x: int):
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L881)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1066)
</details>
## `unavailable-implicit-super-arguments`
@@ -1164,7 +1341,7 @@ class A:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L924)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1109)
</details>
## `unknown-argument`
@@ -1190,7 +1367,7 @@ f(x=1, y=2) # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L979)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1166)
</details>
## `unresolved-attribute`
@@ -1204,11 +1381,20 @@ f(x=1, y=2) # Error raised here
Checks for unresolved attributes.
### Why is this bad?
Accessing an unbound attribute will raise an `AttributeError` at runtime. An unresolved attribute is not guaranteed to exist from the type alone, so this could also indicate that the object is not of the type that the user expects.
Accessing an unbound attribute will raise an `AttributeError` at runtime.
An unresolved attribute is not guaranteed to exist from the type alone,
so this could also indicate that the object is not of the type that the user expects.
### Examples
```python
class A: ...
A().foo # AttributeError: 'A' object has no attribute 'foo'
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1000)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1187)
</details>
## `unresolved-import`
@@ -1222,12 +1408,17 @@ Accessing an unbound attribute will raise an `AttributeError` at runtime. An unr
Checks for import statements for which the module cannot be resolved.
### Why is this bad?
Importing a module that cannot be resolved will raise an `ImportError`
Importing a module that cannot be resolved will raise a `ModuleNotFoundError`
at runtime.
### Examples
```python
import foo # ModuleNotFoundError: No module named 'foo'
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1013)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1209)
</details>
## `unresolved-reference`
@@ -1251,7 +1442,7 @@ print(x) # NameError: name 'x' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1027)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1228)
</details>
## `unsupported-bool-conversion`
@@ -1287,7 +1478,7 @@ b1 < b2 < b1 # exception raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L757)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L921)
</details>
## `unsupported-operator`
@@ -1305,9 +1496,16 @@ the operands don't support the operator.
Attempting to use an unsupported operator will raise a `TypeError` at
runtime.
### Examples
```python
class A: ...
A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1046)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1247)
</details>
## `zero-stepsize-in-slice`
@@ -1331,7 +1529,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1269)
</details>
## `call-possibly-unbound-method`
@@ -1349,7 +1547,7 @@ Calling an unbound method will raise an `AttributeError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-possibly-unbound-method)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L102)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L103)
</details>
## `invalid-ignore-comment`
@@ -1394,9 +1592,18 @@ Checks for possibly unbound attributes.
### Why is this bad?
Attempting to access an unbound attribute will raise an `AttributeError` at runtime.
### Examples
```python
class A:
if b:
c = 0
A.c # AttributeError: type object 'A' has no attribute 'c'
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L973)
</details>
## `possibly-unbound-import`
@@ -1413,9 +1620,21 @@ Checks for imports of symbols that may be unbound.
Importing an unbound module or name will raise a `ModuleNotFoundError`
or `ImportError` at runtime.
### Examples
```python
## module.py
import datetime
if datetime.date.today().weekday() != 6:
a = 1
## main.py
from module import a # ImportError: cannot import name 'a' from 'module'
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L822)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L995)
</details>
## `redundant-cast`
@@ -1441,7 +1660,7 @@ cast(int, f()) # Redundant
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1118)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1340)
</details>
## `undefined-reveal`
@@ -1458,11 +1677,13 @@ Checks for calls to `reveal_type` without importing it.
Using `reveal_type` without importing it will raise a `NameError` at runtime.
### Examples
TODO #14889
```python
reveal_type(1) # NameError: name 'reveal_type' is not defined
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L963)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1148)
</details>
## `unknown-rule`
@@ -1519,7 +1740,7 @@ print(x) # NameError: name 'x' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L836)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1021)
</details>
## `unused-ignore-comment`

View File

@@ -51,14 +51,20 @@ pub(crate) struct CheckCommand {
#[arg(long, value_name = "PROJECT")]
pub(crate) project: Option<SystemPathBuf>,
/// Path to the Python installation from which ty resolves type information and third-party dependencies.
/// Path to the Python environment.
///
/// If not specified, ty will look at the `VIRTUAL_ENV` environment variable.
/// ty uses the Python environment to resolve type information and third-party dependencies.
///
/// ty will search in the path's `site-packages` directories for type information and
/// third-party imports.
/// If not specified, ty will attempt to infer it from the `VIRTUAL_ENV` environment variable or
/// discover a `.venv` directory in the project root or working directory.
///
/// This option is commonly used to specify the path to a virtual environment.
/// If a path to a Python interpreter is provided, e.g., `.venv/bin/python3`, ty will attempt to
/// find an environment two directories up from the interpreter's path, e.g., `.venv`. At this
/// time, ty does not invoke the interpreter to determine the location of the environment. This
/// means that ty will not resolve dynamic executables such as a shim.
///
/// ty will search in the resolved environments's `site-packages` directories for type
/// information and third-party imports.
#[arg(long, value_name = "PATH")]
pub(crate) python: Option<SystemPathBuf>,
@@ -71,6 +77,15 @@ pub(crate) struct CheckCommand {
pub(crate) extra_search_path: Option<Vec<SystemPathBuf>>,
/// Python version to assume when resolving types.
///
/// The Python version affects allowed syntax, type definitions of the standard library, and
/// type definitions of first- and third-party modules that are conditional on the Python version.
///
/// By default, the Python version is inferred as the lower bound of the project's
/// `requires-python` field from the `pyproject.toml`, if available. Otherwise, the latest
/// stable version supported by ty is used, which is currently 3.13.
///
/// ty will not infer the Python version from the Python environment at this time.
#[arg(long, value_name = "VERSION", alias = "target-version")]
pub(crate) python_version: Option<PythonVersion>,

View File

@@ -25,7 +25,7 @@ use ruff_db::Upcast;
use salsa::plumbing::ZalsaDatabase;
use ty_project::metadata::options::Options;
use ty_project::watch::ProjectWatcher;
use ty_project::{watch, Db};
use ty_project::{watch, Db, DummyReporter, Reporter};
use ty_project::{ProjectDatabase, ProjectMetadata};
use ty_server::run_server;
@@ -62,6 +62,11 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
countme::enable(verbosity.is_trace());
let _guard = setup_tracing(verbosity)?;
tracing::warn!(
"ty is pre-release software and not ready for production use. \
Expect to encounter bugs, missing features, and fatal errors.",
);
tracing::debug!("Version: {}", version::version());
// The base path to which all CLI arguments are relative to.
@@ -200,22 +205,34 @@ impl MainLoop {
self.watcher = Some(ProjectWatcher::new(watcher, db));
self.run(db)?;
// Do not show progress bars with `--watch`, indicatif does not seem to
// handle cancelling independent progress bars very well.
self.run_with_progress::<DummyReporter>(db)?;
Ok(ExitStatus::Success)
}
fn run(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
fn run(self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
self.run_with_progress::<IndicatifReporter>(db)
}
fn run_with_progress<R>(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus>
where
R: Reporter + Default + 'static,
{
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
let result = self.main_loop(db);
let result = self.main_loop::<R>(db);
tracing::debug!("Exiting main loop");
result
}
fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
fn main_loop<R>(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus>
where
R: Reporter + Default + 'static,
{
// Schedule the first check.
tracing::debug!("Starting main loop");
@@ -226,11 +243,12 @@ impl MainLoop {
MainLoopMessage::CheckWorkspace => {
let db = db.clone();
let sender = self.sender.clone();
let mut reporter = R::default();
// Spawn a new task that checks the project. This needs to be done in a separate thread
// to prevent blocking the main loop here.
rayon::spawn(move || {
match db.check() {
match db.check_with_reporter(&mut reporter) {
Ok(result) => {
// Send the result back to the main loop for printing.
sender
@@ -340,6 +358,32 @@ impl MainLoop {
}
}
/// A progress reporter for `ty check`.
#[derive(Default)]
struct IndicatifReporter(Option<indicatif::ProgressBar>);
impl ty_project::Reporter for IndicatifReporter {
fn set_files(&mut self, files: usize) {
let progress = indicatif::ProgressBar::new(files as u64);
progress.set_style(
indicatif::ProgressStyle::with_template(
"{msg:8.dim} {bar:60.green/dim} {pos}/{len} files",
)
.unwrap()
.progress_chars("--"),
);
progress.set_message("Checking");
self.0 = Some(progress);
}
fn report_file(&self, _file: &ruff_db::files::File) {
if let Some(ref progress_bar) = self.0 {
progress_bar.inc(1);
}
}
}
#[derive(Debug)]
struct MainLoopCancellationToken {
sender: crossbeam_channel::Sender<MainLoopMessage>,

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