Compare commits

...

92 Commits

Author SHA1 Message Date
Carl Meyer
381b7d0982 re-add list and tuple to property tests 2025-05-12 17:50:58 -07:00
Carl Meyer
f68dbfdef1 [ty] recognize non-fully-static specializations 2025-05-12 16:34:14 -07: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
Dylan
2370297cde Bump 0.11.9 (#17986) 2025-05-09 10:43:27 -05:00
Alex Waygood
a137cb18d4 [ty] Display "All checks passed!" message in green (#17982) 2025-05-09 14:29:43 +01:00
Alex Waygood
03a4d56624 [ty] Change range of revealed-type diagnostic to be the range of the argument passed in, not the whole call (#17980) 2025-05-09 14:15:39 +01:00
David Peter
642eac452d [ty] Recursive protocols (#17929)
## Summary

Use a self-reference "marker" ~~and fixpoint iteration~~ to solve the
stack overflow problems with recursive protocols. This is not pretty and
somewhat tedious, but seems to work fine. Much better than all my
fixpoint-iteration attempts anyway.

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

## Test Plan

New Markdown tests.
2025-05-09 14:54:02 +02:00
Micha Reiser
c1b875799b [ty] CLI reference (#17978) 2025-05-09 14:23:24 +02:00
Micha Reiser
6cd8a49638 [ty] Update salsa (#17964) 2025-05-09 11:54:07 +02:00
Micha Reiser
12ce445ff7 [ty] Document configuration schema (#17950) 2025-05-09 10:47:45 +02:00
justin
f46ed8d410 [ty] Add --config CLI arg (#17697) 2025-05-09 08:38:37 +02:00
Carl Meyer
6c177e2bbe [ty] primer updates (#17903)
## Summary

Update ecosystem project lists in light of
https://github.com/astral-sh/ruff/pull/17758

## Test Plan

CI on this PR.
2025-05-08 20:43:31 -07:00
Carl Meyer
3d2485eb1b [ty] fix more ecosystem/fuzzer panics with fixpoint (#17758)
## Summary

Add cycle handling for `try_metaclass` and `pep695_generic_context`
queries, as well as adjusting the cycle handling for `try_mro` to ensure
that it short-circuits on cycles and won't grow MROs indefinitely.

This reduces the number of failing fuzzer seeds from 68 to 17. The
latter count includes fuzzer seeds 120, 160, and 335, all of which
previously panicked but now either hang or are very slow; I've
temporarily skipped those seeds in the fuzzer until I can dig into that
slowness further.

This also allows us to move some more ecosystem projects from `bad.txt`
to `good.txt`, which I've done in
https://github.com/astral-sh/ruff/pull/17903

## Test Plan

Added mdtests.
2025-05-08 20:36:20 -07:00
Douglas Creager
f78367979e [ty] Remove SliceLiteral type variant (#17958)
@AlexWaygood pointed out that the `SliceLiteral` type variant was
originally created to handle slices before we had generics.
https://github.com/astral-sh/ruff/pull/17927#discussion_r2078115787

Now that we _do_ have generics, we can use a specialization of the
`slice` builtin type for slice literals.

This depends on https://github.com/astral-sh/ruff/pull/17956, since we
need to make sure that all typevar defaults are fully substituted when
specializing `slice`.
2025-05-08 20:16:41 -04:00
Douglas Creager
b705664d49 [ty] Handle typevars that have other typevars as a default (#17956)
It's possible for a typevar to list another typevar as its default
value:

```py
class C[T, U = T]: ...
```

When specializing this class, if a type isn't provided for `U`, we would
previously use the default as-is, leaving an unspecialized `T` typevar
in the specialization. Instead, we want to use what `T` is mapped to as
the type of `U`.

```py
reveal_type(C())  # revealed: C[Unknown, Unknown]
reveal_type(C[int]())  # revealed: C[int, int]
reveal_type(C[int, str]())  # revealed: C[int, str]
```

This is especially important for the `slice` built-in type.
2025-05-08 19:01:27 -04:00
Alex Waygood
f51f1f7153 [ty] Support extending __all__ from an imported module even when the module is not an ExprName node (#17947) 2025-05-08 23:54:19 +01:00
Alex Waygood
9b694ada82 [ty] Report duplicate Protocol or Generic base classes with [duplicate-base], not [inconsistent-mro] (#17971) 2025-05-08 23:41:22 +01:00
Alex Waygood
4d81a41107 [ty] Respect the gradual guarantee when reporting errors in resolving MROs (#17962) 2025-05-08 22:57:39 +01:00
Brent Westbrook
981bd70d39 Convert Message::SyntaxError to use Diagnostic internally (#17784)
## Summary

This PR is a first step toward integration of the new `Diagnostic` type
into ruff. There are two main changes:
- A new `UnifiedFile` enum wrapping `File` for red-knot and a
`SourceFile` for ruff
- ruff's `Message::SyntaxError` variant is now a `Diagnostic` instead of
a `SyntaxErrorMessage`

The second of these changes was mostly just a proof of concept for the
first, and it went pretty smoothly. Converting `DiagnosticMessage`s will
be most of the work in replacing `Message` entirely.

## Test Plan

Existing tests, which show no changes.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-08 12:45:51 -04:00
Alex Waygood
0763331f7f [ty] Support extending __all__ with a literal tuple or set as well as a literal list (#17948) 2025-05-08 17:37:25 +01:00
Alex Waygood
da8540862d [ty] Make unused-ignore-comment disabled by default for now (#17955) 2025-05-08 17:21:34 +01:00
Micha Reiser
6a5533c44c [ty] Change default severity for unbound-reference to error (#17936) 2025-05-08 17:54:46 +02:00
Micha Reiser
d608eae126 [ty] Ignore possibly-unresolved-reference by default (#17934) 2025-05-08 17:44:56 +02:00
Micha Reiser
067a8ac574 [ty] Default to latest supported python version (#17938) 2025-05-08 16:58:35 +02:00
Micha Reiser
5eb215e8e5 [ty] Generate and add rules table (#17953) 2025-05-08 16:55:39 +02:00
Zanie Blue
91aa853b9c Update the schemastore script to match changes in ty (#17952)
See https://github.com/astral-sh/ty/pull/273
2025-05-08 09:31:52 -05:00
Brent Westbrook
57bf7dfbd9 [ty] Implement global handling and load-before-global-declaration syntax error (#17637)
Summary
--

This PR resolves both the typing-related and syntax error TODOs added in
#17563 by tracking a set of `global` bindings for each scope. As
discussed below, we avoid the additional AST traversal from ruff by
collecting `Name`s from `global` statements while building the semantic
index and emit a syntax error if the `Name` is already bound in the
current scope at the point of the `global` statement. This has the
downside of separating the error from the `SemanticSyntaxChecker`, but I
plan to explore using this approach in the `SemanticSyntaxChecker`
itself as a follow-up. It seems like this may be a better approach for
ruff as well.

Test Plan
--

Updated all of the related mdtests to remove the TODOs (and add quotes I
forgot on the messages).

There is one remaining TODO, but it requires `nonlocal` support, which
isn't even incorporated into the `SemanticSyntaxChecker` yet.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-08 10:30:04 -04:00
Alex Waygood
67cd94ed64 [ty] Add missing bitwise-operator branches for boolean and integer arithmetic (#17949) 2025-05-08 14:10:35 +01:00
Wei Lee
aac862822f [airflow] Fix SQLTableCheckOperator typo (AIR302) (#17946) 2025-05-08 14:34:55 +02:00
Micha Reiser
3755ac9fac Update ty metadata (#17943) 2025-05-08 13:24:31 +02:00
David Peter
4f890b2867 [ty] Update salsa (#17937)
## Summary

* Update salsa to pull in https://github.com/salsa-rs/salsa/pull/850.
* Some refactoring of salsa event callbacks in various `Db`'s due to
https://github.com/salsa-rs/salsa/pull/849

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

## Test Plan

Ran `cargo run --bin ty -- -vvv` on a test file to make sure that salsa
Events are still logged.
2025-05-08 12:02:53 +02:00
350 changed files with 10943 additions and 3709 deletions

7
.github/mypy-primer-ty.toml vendored Normal file
View File

@@ -0,0 +1,7 @@
#:schema ../ty.schema.json
# Configuration overrides for the mypy primer run
# Enable off-by-default rules.
[rules]
possibly-unresolved-reference = "warn"
unused-ignore-comment = "warn"

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

@@ -50,6 +50,10 @@ jobs:
run: |
cd ruff
echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
mkdir -p ~/.config/ty
cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
PRIMER_SELECTOR="$(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt)"
echo "new commit"

View File

@@ -5,6 +5,7 @@ exclude: |
.github/workflows/release.yml|
crates/ty_vendored/vendor/.*|
crates/ty_project/resources/.*|
crates/ty/docs/(configuration|rules|cli).md|
crates/ruff_benchmark/resources/.*|
crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*|
@@ -79,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
@@ -97,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

@@ -1,5 +1,37 @@
# Changelog
## 0.11.9
### Preview features
- Default to latest supported Python version for version-related syntax errors ([#17529](https://github.com/astral-sh/ruff/pull/17529))
- Implement deferred annotations for Python 3.14 ([#17658](https://github.com/astral-sh/ruff/pull/17658))
- \[`airflow`\] Fix `SQLTableCheckOperator` typo (`AIR302`) ([#17946](https://github.com/astral-sh/ruff/pull/17946))
- \[`airflow`\] Remove `airflow.utils.dag_parsing_context.get_parsing_context` (`AIR301`) ([#17852](https://github.com/astral-sh/ruff/pull/17852))
- \[`airflow`\] Skip attribute check in try catch block (`AIR301`) ([#17790](https://github.com/astral-sh/ruff/pull/17790))
- \[`flake8-bandit`\] Mark tuples of string literals as trusted input in `S603` ([#17801](https://github.com/astral-sh/ruff/pull/17801))
- \[`isort`\] Check full module path against project root(s) when categorizing first-party imports ([#16565](https://github.com/astral-sh/ruff/pull/16565))
- \[`ruff`\] Add new rule `in-empty-collection` (`RUF060`) ([#16480](https://github.com/astral-sh/ruff/pull/16480))
### Bug fixes
- Fix missing `combine` call for `lint.typing-extensions` setting ([#17823](https://github.com/astral-sh/ruff/pull/17823))
- \[`flake8-async`\] Fix module name in `ASYNC110`, `ASYNC115`, and `ASYNC116` fixes ([#17774](https://github.com/astral-sh/ruff/pull/17774))
- \[`pyupgrade`\] Add spaces between tokens as necessary to avoid syntax errors in `UP018` autofix ([#17648](https://github.com/astral-sh/ruff/pull/17648))
- \[`refurb`\] Fix false positive for float and complex numbers in `FURB116` ([#17661](https://github.com/astral-sh/ruff/pull/17661))
- [parser] Flag single unparenthesized generator expr with trailing comma in arguments. ([#17893](https://github.com/astral-sh/ruff/pull/17893))
### Documentation
- Add instructions on how to upgrade to a newer Rust version ([#17928](https://github.com/astral-sh/ruff/pull/17928))
- Update code of conduct email address ([#17875](https://github.com/astral-sh/ruff/pull/17875))
- Add fix safety sections to `PLC2801`, `PLR1722`, and `RUF013` ([#17825](https://github.com/astral-sh/ruff/pull/17825), [#17826](https://github.com/astral-sh/ruff/pull/17826), [#17759](https://github.com/astral-sh/ruff/pull/17759))
- Add link to `check-typed-exception` from `S110` and `S112` ([#17786](https://github.com/astral-sh/ruff/pull/17786))
### Other changes
- Allow passing a virtual environment to `ruff analyze graph` ([#17743](https://github.com/astral-sh/ruff/pull/17743))
## 0.11.8
### Preview features

105
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",
]
@@ -904,7 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -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",
@@ -1485,7 +1485,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -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",
@@ -1549,14 +1549,14 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[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",
@@ -1771,6 +1771,15 @@ dependencies = [
"url",
]
[[package]]
name = "markdown"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb"
dependencies = [
"unicode-id",
]
[[package]]
name = "matchers"
version = "0.1.0"
@@ -1871,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"
@@ -2449,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]]
@@ -2549,7 +2570,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.8"
version = "0.11.9"
dependencies = [
"anyhow",
"argfile",
@@ -2583,6 +2604,7 @@ dependencies = [
"ruff_linter",
"ruff_macros",
"ruff_notebook",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_parser",
@@ -2701,6 +2723,7 @@ dependencies = [
"indoc",
"itertools 0.14.0",
"libcst",
"markdown",
"pretty_assertions",
"rayon",
"regex",
@@ -2709,6 +2732,7 @@ dependencies = [
"ruff_formatter",
"ruff_linter",
"ruff_notebook",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_codegen",
"ruff_python_formatter",
@@ -2725,7 +2749,9 @@ dependencies = [
"tracing",
"tracing-indicatif",
"tracing-subscriber",
"ty",
"ty_project",
"url",
]
[[package]]
@@ -2785,7 +2811,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.8"
version = "0.11.9"
dependencies = [
"aho-corasick",
"anyhow",
@@ -2813,6 +2839,7 @@ dependencies = [
"regex",
"ruff_annotate_snippets",
"ruff_cache",
"ruff_db",
"ruff_diagnostics",
"ruff_macros",
"ruff_notebook",
@@ -2874,6 +2901,13 @@ dependencies = [
"uuid",
]
[[package]]
name = "ruff_options_metadata"
version = "0.0.0"
dependencies = [
"serde",
]
[[package]]
name = "ruff_python_ast"
version = "0.0.0"
@@ -3112,11 +3146,11 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.8"
version = "0.11.9"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.2",
"getrandom 0.3.3",
"js-sys",
"log",
"ruff_formatter",
@@ -3160,6 +3194,7 @@ dependencies = [
"ruff_graph",
"ruff_linter",
"ruff_macros",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_semantic",
@@ -3206,7 +3241,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3219,7 +3254,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.3",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3237,7 +3272,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=f78a641d2086695ac0ef96cbe915bf80b5a690f2#f78a641d2086695ac0ef96cbe915bf80b5a690f2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
dependencies = [
"boxcar",
"compact_str",
@@ -3260,12 +3295,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=f78a641d2086695ac0ef96cbe915bf80b5a690f2#f78a641d2086695ac0ef96cbe915bf80b5a690f2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
[[package]]
name = "salsa-macros"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=f78a641d2086695ac0ef96cbe915bf80b5a690f2#f78a641d2086695ac0ef96cbe915bf80b5a690f2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
dependencies = [
"heck",
"proc-macro2",
@@ -3602,10 +3637,10 @@ 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",
"windows-sys 0.52.0",
]
[[package]]
@@ -3961,6 +3996,7 @@ dependencies = [
"crossbeam",
"ctrlc",
"filetime",
"indicatif",
"insta",
"insta-cmd",
"jiff",
@@ -4013,6 +4049,7 @@ dependencies = [
"ruff_cache",
"ruff_db",
"ruff_macros",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_text_size",
@@ -4143,7 +4180,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",
@@ -4219,6 +4256,12 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicode-id"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
[[package]]
name = "unicode-ident"
version = "1.0.18"
@@ -4322,7 +4365,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

@@ -23,6 +23,7 @@ ruff_index = { path = "crates/ruff_index" }
ruff_linter = { path = "crates/ruff_linter" }
ruff_macros = { path = "crates/ruff_macros" }
ruff_notebook = { path = "crates/ruff_notebook" }
ruff_options_metadata = { path = "crates/ruff_options_metadata" }
ruff_python_ast = { path = "crates/ruff_python_ast" }
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
@@ -37,6 +38,7 @@ ruff_source_file = { path = "crates/ruff_source_file" }
ruff_text_size = { path = "crates/ruff_text_size" }
ruff_workspace = { path = "crates/ruff_workspace" }
ty = { path = "crates/ty" }
ty_ide = { path = "crates/ty_ide" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
@@ -124,7 +126,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "f78a641d2086695ac0ef96cbe915bf80b5a690f2" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "7edce6e248f35c8114b4b021cdb474a3fb2813b3" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -182,7 +184,7 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom"]
ignored = ["getrandom", "ruff_options_metadata"]
[workspace.lints.rust]

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.11.8"
version = "0.11.9"
publish = true
authors = { workspace = true }
edition = { workspace = true }
@@ -20,6 +20,7 @@ ruff_graph = { workspace = true, features = ["serde", "clap"] }
ruff_linter = { workspace = true, features = ["clap"] }
ruff_macros = { workspace = true }
ruff_notebook = { workspace = true }
ruff_options_metadata = { workspace = true, features = ["serde"] }
ruff_python_ast = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_parser = { workspace = true }
@@ -83,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

@@ -22,12 +22,12 @@ use ruff_linter::settings::types::{
PythonVersion, UnsafeFixes,
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
use ruff_python_ast as ast;
use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding};
use ruff_text_size::TextRange;
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::{Options, PycodestyleOptions};
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
use ruff_workspace::resolver::ConfigurationTransformer;
use rustc_hash::FxHashMap;
use toml;

View File

@@ -439,7 +439,7 @@ impl LintCacheData {
.map(|msg| {
// Make sure that all message use the same source file.
assert_eq!(
&msg.file,
msg.file,
messages.first().unwrap().source_file(),
"message uses a different source file"
);

View File

@@ -2,10 +2,8 @@ use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
use itertools::Itertools;
use std::str::FromStr;
use ruff_workspace::{
options::Options,
options_base::{OptionField, OptionSet, OptionsMetadata, Visit},
};
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
use ruff_workspace::options::Options;
#[derive(Default)]
struct CollectOptionsVisitor {

View File

@@ -2,8 +2,8 @@ use anyhow::{anyhow, Result};
use crate::args::HelpFormat;
use ruff_options_metadata::OptionsMetadata;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
#[expect(clippy::print_stdout)]
pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> {

View File

@@ -15,7 +15,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource};
use ruff_linter::message::{Message, SyntaxErrorMessage};
use ruff_linter::message::Message;
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
@@ -102,11 +102,7 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::SyntaxError(SyntaxErrorMessage {
message: err.to_string(),
range: TextRange::default(),
file: dummy,
})],
vec![Message::syntax_error(err, TextRange::default(), dummy)],
FxHashMap::default(),
)
}

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

@@ -59,13 +59,40 @@ type KeyDiagnosticFields = (
Severity,
);
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[(
DiagnosticId::lint("unused-ignore-comment"),
Some("/src/tomllib/_parser.py"),
Some(22299..22333),
"Unused blanket `type: ignore` directive",
Severity::Warning,
)];
// left: [
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(8224..8254), "Argument to function `skip_until` is incorrect", Error),
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(16914..16948), "Argument to function `skip_until` is incorrect", Error),
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(17319..17363), "Argument to function `skip_until` is incorrect", Error),
// ]
//right: [
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(8224..8254), "Argument to this function is incorrect", Error),
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(16914..16948), "Argument to this function is incorrect", Error),
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(17319..17363), "Argument to this function is incorrect", Error),
// ]
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[
(
DiagnosticId::lint("invalid-argument-type"),
Some("/src/tomllib/_parser.py"),
Some(8224..8254),
"Argument to function `skip_until` is incorrect",
Severity::Error,
),
(
DiagnosticId::lint("invalid-argument-type"),
Some("/src/tomllib/_parser.py"),
Some(16914..16948),
"Argument to function `skip_until` is incorrect",
Severity::Error,
),
(
DiagnosticId::lint("invalid-argument-type"),
Some("/src/tomllib/_parser.py"),
Some(17319..17363),
"Argument to function `skip_until` is incorrect",
Severity::Error,
),
];
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
SystemPathBuf::from("src").join(file.name())
@@ -203,7 +230,7 @@ fn assert_diagnostics(db: &dyn Db, diagnostics: &[Diagnostic], expected: &[KeyDi
diagnostic.id(),
diagnostic
.primary_span()
.map(|span| span.file())
.map(|span| span.expect_ty_file())
.map(|file| file.path(db).as_str()),
diagnostic
.primary_span()

View File

@@ -1,15 +1,14 @@
use std::{fmt::Formatter, sync::Arc};
use thiserror::Error;
use render::{FileResolver, Input};
use ruff_source_file::{SourceCode, SourceFile};
use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange};
pub use self::render::DisplayDiagnostic;
use crate::files::File;
use crate::Db;
use crate::{files::File, Db};
use self::render::FileResolver;
mod render;
mod stylesheet;
@@ -115,10 +114,9 @@ impl Diagnostic {
/// callers should prefer using this with `write!` instead of `writeln!`.
pub fn display<'a>(
&'a self,
db: &'a dyn Db,
resolver: &'a dyn FileResolver,
config: &'a DisplayDiagnosticConfig,
) -> DisplayDiagnostic<'a> {
let resolver = FileResolver::new(db);
DisplayDiagnostic::new(resolver, config, self)
}
@@ -233,6 +231,16 @@ impl Diagnostic {
self.primary_annotation().map(|ann| ann.tags.as_slice())
}
/// Returns the "primary" span of this diagnostic, panicking if it does not exist.
///
/// This should typically only be used when working with diagnostics in Ruff, where diagnostics
/// are currently required to have a primary span.
///
/// See [`Diagnostic::primary_span`] for more details.
pub fn expect_primary_span(&self) -> Span {
self.primary_span().expect("Expected a primary span")
}
/// Returns a key that can be used to sort two diagnostics into the canonical order
/// in which they should appear when rendered.
pub fn rendering_sort_key<'a>(&'a self, db: &'a dyn Db) -> impl Ord + 'a {
@@ -267,11 +275,7 @@ impl Ord for RenderingSortKey<'_> {
self.diagnostic.primary_span(),
other.diagnostic.primary_span(),
) {
let order = span1
.file()
.path(self.db)
.as_str()
.cmp(span2.file().path(self.db).as_str());
let order = span1.file().path(&self.db).cmp(span2.file().path(&self.db));
if order.is_ne() {
return order;
}
@@ -608,62 +612,84 @@ 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",
})
}
}
}
#[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,
},
pub fn is_invalid_syntax(&self) -> bool {
matches!(self, Self::InvalidSyntax)
}
}
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())
}
}
/// A unified file representation for both ruff and ty.
///
/// Such a representation is needed for rendering [`Diagnostic`]s that can optionally contain
/// [`Annotation`]s with [`Span`]s that need to refer to the text of a file. However, ty and ruff
/// use very different file types: a `Copy`-able salsa-interned [`File`], and a heavier-weight
/// [`SourceFile`], respectively.
///
/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
/// emitting diagnostics from both ty and ruff.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnifiedFile {
Ty(File),
Ruff(SourceFile),
}
impl UnifiedFile {
pub fn path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a str {
match self {
UnifiedFile::Ty(file) => resolver.path(*file),
UnifiedFile::Ruff(file) => file.name(),
}
}
fn diagnostic_source(&self, resolver: &dyn FileResolver) -> DiagnosticSource {
match self {
UnifiedFile::Ty(file) => DiagnosticSource::Ty(resolver.input(*file)),
UnifiedFile::Ruff(file) => DiagnosticSource::Ruff(file.clone()),
}
}
}
/// A unified wrapper for types that can be converted to a [`SourceCode`].
///
/// As with [`UnifiedFile`], ruff and ty use slightly different representations for source code.
/// [`DiagnosticSource`] wraps both of these and provides the single
/// [`DiagnosticSource::as_source_code`] method to produce a [`SourceCode`] with the appropriate
/// lifetimes.
///
/// See [`UnifiedFile::diagnostic_source`] for a way to obtain a [`DiagnosticSource`] from a file
/// and [`FileResolver`].
#[derive(Clone, Debug)]
enum DiagnosticSource {
Ty(Input),
Ruff(SourceFile),
}
impl DiagnosticSource {
/// Returns this input as a `SourceCode` for convenient querying.
fn as_source_code(&self) -> SourceCode {
match self {
DiagnosticSource::Ty(input) => SourceCode::new(input.text.as_str(), &input.line_index),
DiagnosticSource::Ruff(source) => SourceCode::new(source.source_text(), source.index()),
}
}
}
@@ -675,14 +701,14 @@ impl std::fmt::Display for DiagnosticId {
/// the entire file. For example, when the file should be executable but isn't.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
file: File,
file: UnifiedFile,
range: Option<TextRange>,
}
impl Span {
/// Returns the `File` attached to this `Span`.
pub fn file(&self) -> File {
self.file
/// Returns the `UnifiedFile` attached to this `Span`.
pub fn file(&self) -> &UnifiedFile {
&self.file
}
/// Returns the range, if available, attached to this `Span`.
@@ -703,10 +729,38 @@ impl Span {
pub fn with_optional_range(self, range: Option<TextRange>) -> Span {
Span { range, ..self }
}
/// Returns the [`File`] attached to this [`Span`].
///
/// Panics if the file is a [`UnifiedFile::Ruff`] instead of a [`UnifiedFile::Ty`].
pub fn expect_ty_file(&self) -> File {
match self.file {
UnifiedFile::Ty(file) => file,
UnifiedFile::Ruff(_) => panic!("Expected a ty `File`, found a ruff `SourceFile`"),
}
}
/// Returns the [`SourceFile`] attached to this [`Span`].
///
/// Panics if the file is a [`UnifiedFile::Ty`] instead of a [`UnifiedFile::Ruff`].
pub fn expect_ruff_file(&self) -> &SourceFile {
match &self.file {
UnifiedFile::Ty(_) => panic!("Expected a ruff `SourceFile`, found a ty `File`"),
UnifiedFile::Ruff(file) => file,
}
}
}
impl From<File> for Span {
fn from(file: File) -> Span {
let file = UnifiedFile::Ty(file);
Span { file, range: None }
}
}
impl From<SourceFile> for Span {
fn from(file: SourceFile) -> Self {
let file = UnifiedFile::Ruff(file);
Span { file, range: None }
}
}

View File

@@ -16,7 +16,8 @@ use crate::{
};
use super::{
Annotation, Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, Severity, SubDiagnostic,
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
SubDiagnostic,
};
/// A type that implements `std::fmt::Display` for diagnostic rendering.
@@ -30,17 +31,16 @@ use super::{
/// values. When using Salsa, this most commonly corresponds to the lifetime
/// of a Salsa `Db`.
/// * The lifetime of the diagnostic being rendered.
#[derive(Debug)]
pub struct DisplayDiagnostic<'a> {
config: &'a DisplayDiagnosticConfig,
resolver: FileResolver<'a>,
resolver: &'a dyn FileResolver,
annotate_renderer: AnnotateRenderer,
diag: &'a Diagnostic,
}
impl<'a> DisplayDiagnostic<'a> {
pub(crate) fn new(
resolver: FileResolver<'a>,
resolver: &'a dyn FileResolver,
config: &'a DisplayDiagnosticConfig,
diag: &'a Diagnostic,
) -> DisplayDiagnostic<'a> {
@@ -86,11 +86,13 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
write!(
f,
" {path}",
path = fmt_styled(self.resolver.path(span.file()), stylesheet.emphasis)
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
)?;
if let Some(range) = span.range() {
let input = self.resolver.input(span.file());
let start = input.as_source_code().line_column(range.start());
let diagnostic_source = span.file().diagnostic_source(self.resolver);
let start = diagnostic_source
.as_source_code()
.line_column(range.start());
write!(
f,
@@ -115,7 +117,7 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
.emphasis(stylesheet.emphasis)
.none(stylesheet.none);
let resolved = Resolved::new(&self.resolver, self.diag);
let resolved = Resolved::new(self.resolver, self.diag);
let renderable = resolved.to_renderable(self.config.context);
for diag in renderable.diagnostics.iter() {
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
@@ -138,26 +140,23 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
/// both.)
#[derive(Debug)]
struct Resolved<'a> {
id: String,
diagnostics: Vec<ResolvedDiagnostic<'a>>,
}
impl<'a> Resolved<'a> {
/// Creates a new resolved set of diagnostics.
fn new(resolver: &FileResolver<'a>, diag: &'a Diagnostic) -> Resolved<'a> {
fn new(resolver: &'a dyn FileResolver, diag: &'a Diagnostic) -> Resolved<'a> {
let mut diagnostics = vec![];
diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, diag));
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()
@@ -175,6 +174,7 @@ impl<'a> Resolved<'a> {
#[derive(Debug)]
struct ResolvedDiagnostic<'a> {
severity: Severity,
id: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
}
@@ -182,7 +182,7 @@ struct ResolvedDiagnostic<'a> {
impl<'a> ResolvedDiagnostic<'a> {
/// Resolve a single diagnostic.
fn from_diagnostic(
resolver: &FileResolver<'a>,
resolver: &'a dyn FileResolver,
diag: &'a Diagnostic,
) -> ResolvedDiagnostic<'a> {
let annotations: Vec<_> = diag
@@ -190,25 +190,16 @@ impl<'a> ResolvedDiagnostic<'a> {
.annotations
.iter()
.filter_map(|ann| {
let path = resolver.path(ann.span.file);
let input = resolver.input(ann.span.file);
ResolvedAnnotation::new(path, &input, ann)
let path = ann.span.file.path(resolver);
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
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,
}
@@ -216,7 +207,7 @@ impl<'a> ResolvedDiagnostic<'a> {
/// Resolve a single sub-diagnostic.
fn from_sub_diagnostic(
resolver: &FileResolver<'a>,
resolver: &'a dyn FileResolver,
diag: &'a SubDiagnostic,
) -> ResolvedDiagnostic<'a> {
let annotations: Vec<_> = diag
@@ -224,13 +215,14 @@ impl<'a> ResolvedDiagnostic<'a> {
.annotations
.iter()
.filter_map(|ann| {
let path = resolver.path(ann.span.file);
let input = resolver.input(ann.span.file);
ResolvedAnnotation::new(path, &input, ann)
let path = ann.span.file.path(resolver);
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann)
})
.collect();
ResolvedDiagnostic {
severity: diag.inner.severity,
id: None,
message: diag.inner.message.as_str().to_string(),
annotations,
}
@@ -259,10 +251,18 @@ impl<'a> ResolvedDiagnostic<'a> {
continue;
};
let prev_context_ends =
context_after(&prev.input.as_source_code(), context, prev.line_end).get();
let this_context_begins =
context_before(&ann.input.as_source_code(), context, ann.line_start).get();
let prev_context_ends = context_after(
&prev.diagnostic_source.as_source_code(),
context,
prev.line_end,
)
.get();
let this_context_begins = context_before(
&ann.diagnostic_source.as_source_code(),
context,
ann.line_start,
)
.get();
// The boundary case here is when `prev_context_ends`
// is exactly one less than `this_context_begins`. In
// that case, the context windows are adajcent and we
@@ -289,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,
}
@@ -304,7 +305,7 @@ impl<'a> ResolvedDiagnostic<'a> {
#[derive(Debug)]
struct ResolvedAnnotation<'a> {
path: &'a str,
input: Input,
diagnostic_source: DiagnosticSource,
range: TextRange,
line_start: OneIndexed,
line_end: OneIndexed,
@@ -318,8 +319,12 @@ impl<'a> ResolvedAnnotation<'a> {
/// `path` is the path of the file that this annotation points to.
///
/// `input` is the contents of the file that this annotation points to.
fn new(path: &'a str, input: &Input, ann: &'a Annotation) -> Option<ResolvedAnnotation<'a>> {
let source = input.as_source_code();
fn new(
path: &'a str,
diagnostic_source: &DiagnosticSource,
ann: &'a Annotation,
) -> Option<ResolvedAnnotation<'a>> {
let source = diagnostic_source.as_source_code();
let (range, line_start, line_end) = match (ann.span.range(), ann.message.is_some()) {
// An annotation with no range AND no message is probably(?)
// meaningless, but we should try to render it anyway.
@@ -345,7 +350,7 @@ impl<'a> ResolvedAnnotation<'a> {
};
Some(ResolvedAnnotation {
path,
input: input.clone(),
diagnostic_source: diagnostic_source.clone(),
range,
line_start,
line_end,
@@ -364,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>>,
}
@@ -386,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,
@@ -406,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)
}
}
@@ -510,8 +511,8 @@ impl<'r> RenderableSnippet<'r> {
!anns.is_empty(),
"creating a renderable snippet requires a non-zero number of annotations",
);
let input = &anns[0].input;
let source = input.as_source_code();
let diagnostic_source = &anns[0].diagnostic_source;
let source = diagnostic_source.as_source_code();
let has_primary = anns.iter().any(|ann| ann.is_primary);
let line_start = context_before(
@@ -527,7 +528,7 @@ impl<'r> RenderableSnippet<'r> {
let snippet_start = source.line_start(line_start);
let snippet_end = source.line_end(line_end);
let snippet = input
let snippet = diagnostic_source
.as_source_code()
.slice(TextRange::new(snippet_start, snippet_end));
@@ -613,7 +614,7 @@ impl<'r> RenderableAnnotation<'r> {
}
}
/// A type that facilitates the retrieval of source code from a `Span`.
/// A trait that facilitates the retrieval of source code from a `Span`.
///
/// At present, this is tightly coupled with a Salsa database. In the future,
/// it is intended for this resolver to become an abstraction providing a
@@ -628,36 +629,24 @@ impl<'r> RenderableAnnotation<'r> {
/// callers will need to pass in a different "resolver" for turning `Span`s
/// into actual file paths/contents. The infrastructure for this isn't fully in
/// place, but this type serves to demarcate the intended abstraction boundary.
pub(crate) struct FileResolver<'a> {
db: &'a dyn Db,
}
impl<'a> FileResolver<'a> {
/// Creates a new resolver from a Salsa database.
pub(crate) fn new(db: &'a dyn Db) -> FileResolver<'a> {
FileResolver { db }
}
pub trait FileResolver {
/// Returns the path associated with the file given.
fn path(&self, file: File) -> &'a str {
relativize_path(
self.db.system().current_directory(),
file.path(self.db).as_str(),
)
}
fn path(&self, file: File) -> &str;
/// Returns the input contents associated with the file given.
fn input(&self, file: File) -> Input {
Input {
text: source_text(self.db, file),
line_index: line_index(self.db, file),
}
}
fn input(&self, file: File) -> Input;
}
impl std::fmt::Debug for FileResolver<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "<salsa based file resolver>")
impl FileResolver for &dyn Db {
fn path(&self, file: File) -> &str {
relativize_path(self.system().current_directory(), file.path(*self).as_str())
}
fn input(&self, file: File) -> Input {
Input {
text: source_text(*self, file),
line_index: line_index(*self, file),
}
}
}
@@ -667,16 +656,9 @@ impl std::fmt::Debug for FileResolver<'_> {
/// This contains the actual content of that input as well as a
/// line index for efficiently querying its contents.
#[derive(Clone, Debug)]
struct Input {
text: SourceText,
line_index: LineIndex,
}
impl Input {
/// Returns this input as a `SourceCode` for convenient querying.
fn as_source_code(&self) -> SourceCode<'_, '_> {
SourceCode::new(self.text.as_str(), &self.line_index)
}
pub struct Input {
pub(crate) text: SourceText,
pub(crate) line_index: LineIndex,
}
/// Returns the line number accounting for the given `len`
@@ -730,6 +712,7 @@ mod tests {
use crate::files::system_path_to_file;
use crate::system::{DbWithWritableSystem, SystemPath};
use crate::tests::TestDb;
use crate::Upcast;
use super::*;
@@ -803,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
@@ -827,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
@@ -847,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
@@ -874,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
@@ -893,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
@@ -914,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 | ΔΔΔΔΔΔΔΔΔΔΔΔ
@@ -933,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 | ☃☃☃☃☃☃☃☃☃☃☃☃
@@ -957,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
@@ -974,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
@@ -989,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
@@ -1006,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
@@ -1023,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
@@ -1056,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
@@ -1100,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
@@ -1125,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
@@ -1153,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
@@ -1181,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
@@ -1206,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
@@ -1237,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
@@ -1275,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
@@ -1292,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
@@ -1310,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
@@ -1350,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
@@ -1379,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
@@ -1416,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
@@ -1453,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
@@ -1481,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
@@ -1517,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
@@ -1556,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
@@ -1604,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
@@ -1640,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
@@ -1663,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
@@ -1683,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
@@ -1703,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
@@ -1725,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
@@ -1757,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
@@ -1786,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
@@ -1817,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
@@ -1852,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
@@ -1880,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
@@ -1912,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
@@ -1934,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
@@ -1967,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
@@ -2007,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
@@ -2051,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
@@ -2086,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
@@ -2174,8 +2157,9 @@ watermelon
fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span {
let span = self.path(path);
let text = source_text(&self.db, span.file());
let line_index = line_index(&self.db, span.file());
let file = span.expect_ty_file();
let text = source_text(&self.db, file);
let line_index = line_index(&self.db, file);
let source = SourceCode::new(text.as_str(), &line_index);
let (line_start, offset_start) = parse_line_offset(line_offset_start);
@@ -2237,7 +2221,7 @@ watermelon
///
/// (This will set the "printed" flag on `Diagnostic`.)
fn render(&self, diag: &Diagnostic) -> String {
diag.display(&self.db, &self.config).to_string()
diag.display(&self.db.upcast(), &self.config).to_string()
}
}

View File

@@ -277,7 +277,7 @@ impl std::panic::RefUnwindSafe for Files {}
#[salsa::input]
pub struct File {
/// The path of the file (immutable).
#[return_ref]
#[returns(ref)]
pub path: FilePath,
/// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows

View File

@@ -19,8 +19,8 @@ use crate::Db;
#[salsa::input(debug)]
pub struct FileRoot {
/// The path of a root is guaranteed to never change.
#[return_ref]
path_buf: SystemPathBuf,
#[returns(deref)]
pub path: SystemPathBuf,
/// The kind of the root at the time of its creation.
kind_at_time_of_creation: FileRootKind,
@@ -32,10 +32,6 @@ pub struct FileRoot {
}
impl FileRoot {
pub fn path(self, db: &dyn Db) -> &SystemPath {
self.path_buf(db)
}
pub fn durability(self, db: &dyn Db) -> salsa::Durability {
self.kind_at_time_of_creation(db).durability()
}

View File

@@ -61,13 +61,15 @@ pub fn max_parallelism() -> NonZeroUsize {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use crate::files::Files;
use crate::system::TestSystem;
use crate::system::{DbWithTestSystem, System};
use crate::vendored::VendoredFileSystem;
use crate::Db;
use crate::{Db, Upcast};
type Events = Arc<Mutex<Vec<salsa::Event>>>;
/// Database that can be used for testing.
///
@@ -79,36 +81,37 @@ mod tests {
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
events: Events,
}
impl TestDb {
pub(crate) fn new() -> Self {
let events = Events::default();
Self {
storage: salsa::Storage::default(),
storage: salsa::Storage::new(Some(Box::new({
let events = events.clone();
move |event| {
tracing::trace!("event: {:?}", event);
let mut events = events.lock().unwrap();
events.push(event);
}
}))),
system: TestSystem::default(),
vendored: VendoredFileSystem::default(),
events: std::sync::Arc::default(),
events,
files: Files::default(),
}
}
/// Empties the internal store of salsa events that have been emitted,
/// and returns them as a `Vec` (equivalent to [`std::mem::take`]).
///
/// ## Panics
/// If there are pending database snapshots.
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
let inner = Arc::get_mut(&mut self.events)
.expect("expected no pending salsa database snapshots.");
let mut events = self.events.lock().unwrap();
std::mem::take(inner.get_mut().unwrap())
std::mem::take(&mut *events)
}
/// Clears the emitted salsa events.
///
/// ## Panics
/// If there are pending database snapshots.
pub(crate) fn clear_salsa_events(&mut self) {
self.take_salsa_events();
}
@@ -133,7 +136,16 @@ mod tests {
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
ruff_python_ast::PythonVersion::latest()
ruff_python_ast::PythonVersion::latest_ty()
}
}
impl Upcast<dyn Db> for TestDb {
fn upcast(&self) -> &(dyn Db + 'static) {
self
}
fn upcast_mut(&mut self) -> &mut (dyn Db + 'static) {
self
}
}
@@ -148,12 +160,5 @@ mod tests {
}
#[salsa::db]
impl salsa::Database for TestDb {
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
let event = event();
tracing::trace!("event: {:?}", event);
let mut events = self.events.lock().unwrap();
events.push(event);
}
}
impl salsa::Database for TestDb {}
}

View File

@@ -20,7 +20,7 @@ use crate::Db;
/// reflected in the changed AST offsets.
/// The other reason is that Ruff's AST doesn't implement `Eq` which Sala requires
/// for determining if a query result is unchanged.
#[salsa::tracked(return_ref, no_eq)]
#[salsa::tracked(returns(ref), no_eq)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let _span = tracing::trace_span!("parsed_module", ?file).entered();

View File

@@ -11,12 +11,14 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
ty = { workspace = true }
ty_project = { workspace = true, features = ["schemars"] }
ruff = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true, features = ["schemars"] }
ruff_notebook = { workspace = true }
ruff_options_metadata = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_codegen = { workspace = true }
ruff_python_formatter = { workspace = true }
@@ -31,6 +33,7 @@ imara-diff = { workspace = true }
indicatif = { workspace = true }
itertools = { workspace = true }
libcst = { workspace = true }
markdown = { version = "1.0.0" }
pretty_assertions = { workspace = true }
rayon = { workspace = true }
regex = { workspace = true }
@@ -44,6 +47,7 @@ toml = { workspace = true, features = ["parse"] }
tracing = { workspace = true }
tracing-indicatif = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
url = { workspace = true }
[dev-dependencies]
indoc = { workspace = true }

View File

@@ -2,7 +2,10 @@
use anyhow::Result;
use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_ty_schema};
use crate::{
generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference,
generate_ty_options, generate_ty_rules, generate_ty_schema,
};
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
@@ -38,5 +41,8 @@ pub(crate) fn main(args: &Args) -> Result<()> {
generate_docs::main(&generate_docs::Args {
dry_run: args.mode.is_dry_run(),
})?;
generate_ty_options::main(&generate_ty_options::Args { mode: args.mode })?;
generate_ty_rules::main(&generate_ty_rules::Args { mode: args.mode })?;
generate_ty_cli_reference::main(&generate_ty_cli_reference::Args { mode: args.mode })?;
Ok(())
}

View File

@@ -1,5 +1,4 @@
//! Generate CLI help.
#![allow(clippy::print_stdout)]
use std::path::PathBuf;
use std::{fs, str};

View File

@@ -1,5 +1,4 @@
//! Generate Markdown documentation for applicable rules.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::collections::HashSet;
use std::fmt::Write as _;
@@ -13,8 +12,8 @@ use strum::IntoEnumIterator;
use ruff_diagnostics::FixAvailability;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
use ruff_workspace::options::Options;
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
use crate::ROOT_DIR;

View File

@@ -1,5 +1,3 @@
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

View File

@@ -4,9 +4,9 @@
use itertools::Itertools;
use std::fmt::Write;
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
use ruff_python_trivia::textwrap;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
pub(crate) fn generate() -> String {
let mut output = String::new();

View File

@@ -11,8 +11,8 @@ use strum::IntoEnumIterator;
use ruff_diagnostics::FixAvailability;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
use ruff_options_metadata::OptionsMetadata;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
const FIX_SYMBOL: &str = "🛠️";
const PREVIEW_SYMBOL: &str = "🧪";

View File

@@ -0,0 +1,334 @@
//! Generate a Markdown-compatible reference for the ty command-line interface.
use std::cmp::max;
use std::path::PathBuf;
use anyhow::{bail, Result};
use clap::{Command, CommandFactory};
use itertools::Itertools;
use pretty_assertions::StrComparison;
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
use crate::ROOT_DIR;
use ty::Cli;
const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"];
#[derive(clap::Args)]
pub(crate) struct Args {
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
pub(crate) fn main(args: &Args) -> Result<()> {
let reference_string = generate();
let filename = "crates/ty/docs/cli.md";
let reference_path = PathBuf::from(ROOT_DIR).join(filename);
match args.mode {
Mode::DryRun => {
println!("{reference_string}");
}
Mode::Check => {
match std::fs::read_to_string(reference_path) {
Ok(current) => {
if current == reference_string {
println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &reference_string);
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("{filename} not found, please run `{REGENERATE_ALL_COMMAND}`");
}
Err(err) => {
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{err}");
}
}
}
Mode::Write => match std::fs::read_to_string(&reference_path) {
Ok(current) => {
if current == reference_string {
println!("Up-to-date: {filename}");
} else {
println!("Updating: {filename}");
std::fs::write(reference_path, reference_string.as_bytes())?;
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
println!("Updating: {filename}");
std::fs::write(reference_path, reference_string.as_bytes())?;
}
Err(err) => {
bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}");
}
},
}
Ok(())
}
fn generate() -> String {
let mut output = String::new();
let mut ty = Cli::command();
// It is very important to build the command before beginning inspection or subcommands
// will be missing all of the propagated options.
ty.build();
let mut parents = Vec::new();
output.push_str("# CLI Reference\n\n");
generate_command(&mut output, &ty, &mut parents);
output
}
#[allow(clippy::format_push_string)]
fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut Vec<&'a Command>) {
if command.is_hide_set() && !SHOW_HIDDEN_COMMANDS.contains(&command.get_name()) {
return;
}
// Generate the command header.
let name = if parents.is_empty() {
command.get_name().to_string()
} else {
format!(
"{} {}",
parents.iter().map(|cmd| cmd.get_name()).join(" "),
command.get_name()
)
};
// Display the top-level `ty` command at the same level as its children
let level = max(2, parents.len() + 1);
output.push_str(&format!("{} {name}\n\n", "#".repeat(level)));
// Display the command description.
if let Some(about) = command.get_long_about().or_else(|| command.get_about()) {
output.push_str(&about.to_string());
output.push_str("\n\n");
}
// Display the usage
{
// This appears to be the simplest way to get rendered usage from Clap,
// it is complicated to render it manually. It's annoying that it
// requires a mutable reference but it doesn't really matter.
let mut command = command.clone();
output.push_str("<h3 class=\"cli-reference\">Usage</h3>\n\n");
output.push_str(&format!(
"```\n{}\n```",
command
.render_usage()
.to_string()
.trim_start_matches("Usage: "),
));
output.push_str("\n\n");
}
if command.get_name() == "help" {
return;
}
// Display a list of child commands
let mut subcommands = command.get_subcommands().peekable();
let has_subcommands = subcommands.peek().is_some();
if has_subcommands {
output.push_str("<h3 class=\"cli-reference\">Commands</h3>\n\n");
output.push_str("<dl class=\"cli-reference\">");
for subcommand in subcommands {
if subcommand.is_hide_set() {
continue;
}
let subcommand_name = format!("{name} {}", subcommand.get_name());
output.push_str(&format!(
"<dt><a href=\"#{}\"><code>{subcommand_name}</code></a></dt>",
subcommand_name.replace(' ', "-")
));
if let Some(about) = subcommand.get_about() {
output.push_str(&format!(
"<dd>{}</dd>\n",
markdown::to_html(&about.to_string())
));
}
}
output.push_str("</dl>\n\n");
}
// Do not display options for commands with children
if !has_subcommands {
let name_key = name.replace(' ', "-");
// Display positional arguments
let mut arguments = command
.get_positionals()
.filter(|arg| !arg.is_hide_set())
.peekable();
if arguments.peek().is_some() {
output.push_str("<h3 class=\"cli-reference\">Arguments</h3>\n\n");
output.push_str("<dl class=\"cli-reference\">");
for arg in arguments {
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>",
arg.get_id().to_string().to_uppercase(),
));
output.push_str("</dt>");
if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) {
output.push_str("<dd>");
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
output.push_str("</dd>");
}
}
output.push_str("</dl>\n\n");
}
// Display options and flags
let mut options = command
.get_arguments()
.filter(|arg| !arg.is_positional())
.filter(|arg| !arg.is_hide_set())
.sorted_by_key(|arg| arg.get_id())
.peekable();
if options.peek().is_some() {
output.push_str("<h3 class=\"cli-reference\">Options</h3>\n\n");
output.push_str("<dl class=\"cli-reference\">");
for opt in options {
let Some(long) = opt.get_long() else { continue };
let id = format!("{name_key}--{long}");
output.push_str(&format!("<dt id=\"{id}\">"));
output.push_str(&format!("<a href=\"#{id}\"><code>--{long}</code></a>"));
for long_alias in opt.get_all_aliases().into_iter().flatten() {
output.push_str(&format!(", <code>--{long_alias}</code>"));
}
if let Some(short) = opt.get_short() {
output.push_str(&format!(", <code>-{short}</code>"));
}
for short_alias in opt.get_all_short_aliases().into_iter().flatten() {
output.push_str(&format!(", <code>-{short_alias}</code>"));
}
// Re-implements private `Arg::is_takes_value_set` used in `Command::get_opts`
if opt
.get_num_args()
.unwrap_or_else(|| 1.into())
.takes_values()
{
if let Some(values) = opt.get_value_names() {
for value in values {
output.push_str(&format!(
" <i>{}</i>",
value.to_lowercase().replace('_', "-")
));
}
}
}
output.push_str("</dt>");
if let Some(help) = opt.get_long_help().or_else(|| opt.get_help()) {
output.push_str("<dd>");
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
emit_env_option(opt, output);
emit_default_option(opt, output);
emit_possible_options(opt, output);
output.push_str("</dd>");
}
}
output.push_str("</dl>");
}
output.push_str("\n\n");
}
parents.push(command);
// Recurse to all of the subcommands.
for subcommand in command.get_subcommands() {
generate_command(output, subcommand, parents);
}
parents.pop();
}
fn emit_env_option(opt: &clap::Arg, output: &mut String) {
if opt.is_hide_env_set() {
return;
}
if let Some(env) = opt.get_env() {
output.push_str(&markdown::to_html(&format!(
"May also be set with the `{}` environment variable.",
env.to_string_lossy()
)));
}
}
fn emit_default_option(opt: &clap::Arg, output: &mut String) {
if opt.is_hide_default_value_set() || !opt.get_num_args().expect("built").takes_values() {
return;
}
let values = opt.get_default_values();
if !values.is_empty() {
let value = format!(
"\n[default: {}]",
opt.get_default_values()
.iter()
.map(|s| s.to_string_lossy())
.join(",")
);
output.push_str(&markdown::to_html(&value));
}
}
fn emit_possible_options(opt: &clap::Arg, output: &mut String) {
if opt.is_hide_possible_values_set() {
return;
}
let values = opt.get_possible_values();
if !values.is_empty() {
let value = format!(
"\nPossible values:\n{}",
values
.into_iter()
.filter(|value| !value.is_hide_set())
.map(|value| {
let name = value.get_name();
value.get_help().map_or_else(
|| format!(" - `{name}`"),
|help| format!(" - `{name}`: {help}"),
)
})
.collect_vec()
.join("\n"),
);
output.push_str(&markdown::to_html(&value));
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::generate_all::Mode;
use super::{main, Args};
#[test]
fn ty_cli_reference_is_up_to_date() -> Result<()> {
main(&Args { mode: Mode::Check })
}
}

View File

@@ -0,0 +1,257 @@
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
use anyhow::bail;
use itertools::Itertools;
use pretty_assertions::StrComparison;
use std::{fmt::Write, path::PathBuf};
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
use ty_project::metadata::Options;
use crate::{
generate_all::{Mode, REGENERATE_ALL_COMMAND},
ROOT_DIR,
};
#[derive(clap::Args)]
pub(crate) struct Args {
/// Write the generated table to stdout (rather than to `crates/ty/docs/configuration.md`).
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
let mut output = String::new();
let file_name = "crates/ty/docs/configuration.md";
let markdown_path = PathBuf::from(ROOT_DIR).join(file_name);
generate_set(
&mut output,
Set::Toplevel(Options::metadata()),
&mut Vec::new(),
);
match args.mode {
Mode::DryRun => {
println!("{output}");
}
Mode::Check => {
let current = std::fs::read_to_string(&markdown_path)?;
if output == current {
println!("Up-to-date: {file_name}",);
} else {
let comparison = StrComparison::new(&current, &output);
bail!("{file_name} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}",);
}
}
Mode::Write => {
let current = std::fs::read_to_string(&markdown_path)?;
if current == output {
println!("Up-to-date: {file_name}",);
} else {
println!("Updating: {file_name}",);
std::fs::write(markdown_path, output.as_bytes())?;
}
}
}
Ok(())
}
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
match &set {
Set::Toplevel(_) => {
output.push_str("# Configuration\n");
}
Set::Named { name, .. } => {
let title = parents
.iter()
.filter_map(|set| set.name())
.chain(std::iter::once(name.as_str()))
.join(".");
writeln!(output, "## `{title}`\n",).unwrap();
}
}
if let Some(documentation) = set.metadata().documentation() {
output.push_str(documentation);
output.push('\n');
output.push('\n');
}
let mut visitor = CollectOptionsVisitor::default();
set.metadata().record(&mut visitor);
let (mut fields, mut sets) = (visitor.fields, visitor.groups);
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
parents.push(set);
// Generate the fields.
for (name, field) in &fields {
emit_field(output, name, field, parents.as_slice());
output.push_str("---\n\n");
}
// Generate all the sub-sets.
for (set_name, sub_set) in &sets {
generate_set(
output,
Set::Named {
name: set_name.to_string(),
set: *sub_set,
},
parents,
);
}
parents.pop();
}
enum Set {
Toplevel(OptionSet),
Named { name: String, set: OptionSet },
}
impl Set {
fn name(&self) -> Option<&str> {
match self {
Set::Toplevel(_) => None,
Set::Named { name, .. } => Some(name),
}
}
fn metadata(&self) -> &OptionSet {
match self {
Set::Toplevel(set) => set,
Set::Named { set, .. } => 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}`");
output.push('\n');
if let Some(deprecated) = &field.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();
}
output.push('.');
if let Some(message) = deprecated.message {
writeln!(output, " {message}").unwrap();
}
output.push('\n');
}
output.push_str(field.doc);
output.push_str("\n\n");
let _ = writeln!(output, "**Default value**: `{}`", field.default);
output.push('\n');
let _ = writeln!(output, "**Type**: `{}`", field.value_type);
output.push('\n');
output.push_str("**Example usage** (`pyproject.toml`):\n\n");
output.push_str(&format_example(
&format_header(
field.scope,
field.example,
parents,
ConfigurationFile::PyprojectToml,
),
field.example,
));
output.push('\n');
}
fn format_example(header: &str, content: &str) -> String {
if header.is_empty() {
format!("```toml\n{content}\n```\n",)
} else {
format!("```toml\n{header}\n{content}\n```\n",)
}
}
/// Format the TOML header for the example usage for a given option.
///
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
fn format_header(
scope: Option<&str>,
example: &str,
parents: &[Set],
configuration: ConfigurationFile,
) -> String {
let tool_parent = match configuration {
ConfigurationFile::PyprojectToml => Some("tool.ty"),
ConfigurationFile::TyToml => None,
};
let header = tool_parent
.into_iter()
.chain(parents.iter().filter_map(|parent| parent.name()))
.chain(scope)
.join(".");
// Ex) `[[tool.ty.xx]]`
if example.starts_with(&format!("[[{header}")) {
return String::new();
}
// Ex) `[tool.ty.rules]`
if example.starts_with(&format!("[{header}")) {
return String::new();
}
if header.is_empty() {
String::new()
} else {
format!("[{header}]")
}
}
#[derive(Default)]
struct CollectOptionsVisitor {
groups: Vec<(String, OptionSet)>,
fields: Vec<(String, OptionField)>,
}
impl Visit for CollectOptionsVisitor {
fn record_set(&mut self, name: &str, group: OptionSet) {
self.groups.push((name.to_owned(), group));
}
fn record_field(&mut self, name: &str, field: OptionField) {
self.fields.push((name.to_owned(), field));
}
}
#[derive(Debug, Copy, Clone)]
enum ConfigurationFile {
PyprojectToml,
#[expect(dead_code)]
TyToml,
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::generate_all::Mode;
use super::{main, Args};
#[test]
fn ty_configuration_markdown_up_to_date() -> Result<()> {
main(&Args { mode: Mode::Check })?;
Ok(())
}
}

View File

@@ -0,0 +1,143 @@
//! Generates the rules table for ty
use std::borrow::Cow;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
use anyhow::{bail, Result};
use itertools::Itertools as _;
use pretty_assertions::StrComparison;
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
use crate::ROOT_DIR;
#[derive(clap::Args)]
pub(crate) struct Args {
/// Write the generated table to stdout (rather than to `ty.schema.json`).
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
pub(crate) fn main(args: &Args) -> Result<()> {
let markdown = generate_markdown();
let filename = "crates/ty/docs/rules.md";
let schema_path = PathBuf::from(ROOT_DIR).join(filename);
match args.mode {
Mode::DryRun => {
println!("{markdown}");
}
Mode::Check => {
let current = fs::read_to_string(schema_path)?;
if current == markdown {
println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &markdown);
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
}
}
Mode::Write => {
let current = fs::read_to_string(&schema_path)?;
if current == markdown {
println!("Up-to-date: {filename}");
} else {
println!("Updating: {filename}");
fs::write(schema_path, markdown.as_bytes())?;
}
}
}
Ok(())
}
fn generate_markdown() -> String {
let registry = &*ty_project::DEFAULT_LINT_REGISTRY;
let mut output = String::new();
let _ = writeln!(&mut output, "# Rules\n");
let mut lints: Vec<_> = registry.lints().iter().collect();
lints.sort_by(|a, b| {
a.default_level()
.cmp(&b.default_level())
.reverse()
.then_with(|| a.name().cmp(&b.name()))
});
for lint in lints {
let _ = writeln!(&mut output, "## `{rule_name}`\n", rule_name = lint.name());
// Increase the header-level by one
let documentation = lint
.documentation_lines()
.map(|line| {
if line.starts_with('#') {
Cow::Owned(format!("#{line}"))
} else {
Cow::Borrowed(line)
}
})
.join("\n");
let _ = writeln!(
&mut output,
r#"**Default level**: {level}
<details>
<summary>{summary}</summary>
{documentation}
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name})
* [View source](https://github.com/astral-sh/ruff/blob/main/{file}#L{line})
</details>
"#,
level = lint.default_level(),
// GitHub doesn't support markdown in `summary` headers
summary = replace_inline_code(lint.summary()),
encoded_name = url::form_urlencoded::byte_serialize(lint.name().as_str().as_bytes())
.collect::<String>(),
file = url::form_urlencoded::byte_serialize(lint.file().replace('\\', "/").as_bytes())
.collect::<String>(),
line = lint.line(),
);
}
output
}
/// Replaces inline code blocks (`code`) with `<code>code</code>`
fn replace_inline_code(input: &str) -> String {
let mut output = String::new();
let mut parts = input.split('`');
while let Some(before) = parts.next() {
if let Some(between) = parts.next() {
output.push_str(before);
output.push_str("<code>");
output.push_str(between);
output.push_str("</code>");
} else {
output.push_str(before);
}
}
output
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::generate_all::Mode;
use super::{main, Args};
#[test]
fn ty_rules_up_to_date() -> Result<()> {
main(&Args { mode: Mode::Check })
}
}

View File

@@ -1,5 +1,3 @@
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

View File

@@ -2,6 +2,8 @@
//!
//! Within the ruff repository you can run it with `cargo dev`.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use anyhow::Result;
use clap::{Parser, Subcommand};
use ruff::{args::GlobalConfigArgs, check};
@@ -15,6 +17,9 @@ mod generate_docs;
mod generate_json_schema;
mod generate_options;
mod generate_rules_table;
mod generate_ty_cli_reference;
mod generate_ty_options;
mod generate_ty_rules;
mod generate_ty_schema;
mod print_ast;
mod print_cst;
@@ -44,8 +49,10 @@ enum Command {
GenerateTySchema(generate_ty_schema::Args),
/// Generate a Markdown-compatible table of supported lint rules.
GenerateRulesTable,
GenerateTyRules(generate_ty_rules::Args),
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions,
GenerateTyOptions(generate_ty_options::Args),
/// Generate CLI help.
GenerateCliHelp(generate_cli_help::Args),
/// Generate Markdown docs.
@@ -88,7 +95,9 @@ fn main() -> Result<ExitCode> {
Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
Command::GenerateTySchema(args) => generate_ty_schema::main(&args)?,
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?,
Command::GenerateOptions => println!("{}", generate_options::generate()),
Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?,
Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,
Command::GenerateDocs(args) => generate_docs::main(&args)?,
Command::PrintAST(args) => print_ast::main(&args)?,

View File

@@ -1,5 +1,4 @@
//! Print the AST for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::path::PathBuf;

View File

@@ -1,5 +1,4 @@
//! Print the `LibCST` CST for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

View File

@@ -1,5 +1,4 @@
//! Print the token stream for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::path::PathBuf;

View File

@@ -1,5 +1,4 @@
//! Run round-trip source code generation on a given Python or Jupyter notebook file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

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

@@ -88,8 +88,8 @@ impl Db for ModuleDb {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> Arc<RuleSelection> {
self.rule_selection.clone()
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {
@@ -98,6 +98,4 @@ impl Db for ModuleDb {
}
#[salsa::db]
impl salsa::Database for ModuleDb {
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
}
impl salsa::Database for ModuleDb {}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.11.8"
version = "0.11.9"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -15,6 +15,7 @@ license = { workspace = true }
[dependencies]
ruff_annotate_snippets = { workspace = true }
ruff_cache = { workspace = true }
ruff_db = { workspace = true }
ruff_diagnostics = { workspace = true, features = ["serde"] }
ruff_notebook = { workspace = true }
ruff_macros = { workspace = true }

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

@@ -91,14 +91,14 @@ from airflow.operators.sql import (
BaseSQLOperator,
BranchSQLOperator,
SQLColumnCheckOperator,
SQLTablecheckOperator,
SQLTableCheckOperator,
_convert_to_float_if_possible,
parse_boolean,
)
BaseSQLOperator()
BranchSQLOperator()
SQLTablecheckOperator()
SQLTableCheckOperator()
SQLColumnCheckOperator()
_convert_to_float_if_possible()
parse_boolean()

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

@@ -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,7 @@ 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))

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

@@ -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

@@ -17,7 +17,7 @@ impl Emitter for AzureEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let location = if context.is_notebook(message.filename()) {
let location = if context.is_notebook(&message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()

View File

@@ -22,7 +22,7 @@ use crate::text_helpers::ShowNonprinting;
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
pub(super) struct Diff<'a> {
fix: &'a Fix,
source_code: &'a SourceFile,
source_code: SourceFile,
}
impl<'a> Diff<'a> {

View File

@@ -19,7 +19,7 @@ impl Emitter for GithubEmitter {
) -> anyhow::Result<()> {
for message in messages {
let source_location = message.compute_start_location();
let location = if context.is_notebook(message.filename()) {
let location = if context.is_notebook(&message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()
@@ -43,7 +43,7 @@ impl Emitter for GithubEmitter {
write!(
writer,
"{path}:{row}:{column}:",
path = relativize_path(message.filename()),
path = relativize_path(&*message.filename()),
row = location.line,
column = location.column,
)?;

View File

@@ -62,7 +62,7 @@ impl Serialize for SerializedMessages<'_> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let lines = if self.context.is_notebook(message.filename()) {
let lines = if self.context.is_notebook(&message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
json!({
@@ -77,8 +77,8 @@ impl Serialize for SerializedMessages<'_> {
};
let path = self.project_dir.as_ref().map_or_else(
|| relativize_path(message.filename()),
|project_dir| relativize_path_to(message.filename(), project_dir),
|| relativize_path(&*message.filename()),
|project_dir| relativize_path_to(&*message.filename(), project_dir),
);
let mut message_fingerprint = fingerprint(message, &path, 0);

View File

@@ -65,7 +65,7 @@ impl Emitter for GroupedEmitter {
let column_length = calculate_print_width(max_column_length);
// Print the filename.
writeln!(writer, "{}:", relativize_path(filename).underline())?;
writeln!(writer, "{}:", relativize_path(&*filename).underline())?;
// Print each message.
for message in messages {
@@ -73,7 +73,7 @@ impl Emitter for GroupedEmitter {
writer,
"{}",
DisplayGroupedMessage {
notebook_index: context.notebook_index(message.filename()),
notebook_index: context.notebook_index(&message.filename()),
message,
show_fix_status: self.show_fix_status,
unsafe_fixes: self.unsafe_fixes,

View File

@@ -49,8 +49,9 @@ impl Serialize for ExpandedMessages<'_> {
}
pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value {
let source_code = message.source_file().to_source_code();
let notebook_index = context.notebook_index(message.filename());
let source_file = message.source_file();
let source_code = source_file.to_source_code();
let notebook_index = context.notebook_index(&message.filename());
let fix = message.fix().map(|fix| {
json!({

View File

@@ -32,7 +32,7 @@ impl Emitter for JunitEmitter {
report.add_test_suite(test_suite);
} else {
for (filename, messages) in group_messages_by_filename(messages) {
let mut test_suite = TestSuite::new(filename);
let mut test_suite = TestSuite::new(&filename);
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
@@ -44,7 +44,7 @@ impl Emitter for JunitEmitter {
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.body());
let location = if context.is_notebook(message.filename()) {
let location = if context.is_notebook(&message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()
@@ -66,7 +66,7 @@ impl Emitter for JunitEmitter {
},
status,
);
let file_path = Path::new(filename);
let file_path = Path::new(&*filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
case.set_classname(classname.to_str().unwrap());

View File

@@ -1,8 +1,10 @@
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::io::Write;
use std::ops::Deref;
use ruff_db::diagnostic::{self as db, Annotation, DiagnosticId, Severity, Span};
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
use rustc_hash::FxHashMap;
@@ -45,7 +47,7 @@ mod text;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Message {
Diagnostic(DiagnosticMessage),
SyntaxError(SyntaxErrorMessage),
SyntaxError(db::Diagnostic),
}
/// A diagnostic message corresponding to a rule violation.
@@ -59,14 +61,6 @@ pub struct DiagnosticMessage {
pub noqa_offset: TextSize,
}
/// A syntax error message raised by the parser.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SyntaxErrorMessage {
pub message: String,
pub range: TextRange,
pub file: SourceFile,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum MessageKind {
Diagnostic(Rule),
@@ -83,6 +77,17 @@ impl MessageKind {
}
impl Message {
pub fn syntax_error(
message: impl std::fmt::Display,
range: TextRange,
file: SourceFile,
) -> Message {
let mut diag = db::Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
let span = Span::from(file).with_range(range);
diag.annotate(Annotation::primary(span).message(message));
Self::SyntaxError(diag)
}
/// Create a [`Message`] from the given [`Diagnostic`] corresponding to a rule violation.
pub fn from_diagnostic(
diagnostic: Diagnostic,
@@ -114,14 +119,14 @@ impl Message {
.next()
.map_or(TextSize::new(0), TextLen::text_len);
Message::SyntaxError(SyntaxErrorMessage {
message: format!(
Message::syntax_error(
format_args!(
"SyntaxError: {}",
DisplayParseErrorType::new(&parse_error.error)
),
range: TextRange::at(parse_error.location.start(), len),
TextRange::at(parse_error.location.start(), len),
file,
})
)
}
/// Create a [`Message`] from the given [`UnsupportedSyntaxError`].
@@ -129,11 +134,11 @@ impl Message {
unsupported_syntax_error: &UnsupportedSyntaxError,
file: SourceFile,
) -> Message {
Message::SyntaxError(SyntaxErrorMessage {
message: format!("SyntaxError: {unsupported_syntax_error}"),
range: unsupported_syntax_error.range,
Message::syntax_error(
format_args!("SyntaxError: {unsupported_syntax_error}"),
unsupported_syntax_error.range,
file,
})
)
}
/// Create a [`Message`] from the given [`SemanticSyntaxError`].
@@ -141,11 +146,11 @@ impl Message {
semantic_syntax_error: &SemanticSyntaxError,
file: SourceFile,
) -> Message {
Message::SyntaxError(SyntaxErrorMessage {
message: format!("SyntaxError: {semantic_syntax_error}"),
range: semantic_syntax_error.range,
Message::syntax_error(
format_args!("SyntaxError: {semantic_syntax_error}"),
semantic_syntax_error.range,
file,
})
)
}
pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> {
@@ -168,8 +173,11 @@ impl Message {
}
/// Returns `true` if `self` is a syntax error message.
pub const fn is_syntax_error(&self) -> bool {
matches!(self, Message::SyntaxError(_))
pub fn is_syntax_error(&self) -> bool {
match self {
Message::Diagnostic(_) => false,
Message::SyntaxError(diag) => diag.id().is_invalid_syntax(),
}
}
/// Returns a message kind.
@@ -192,7 +200,11 @@ impl Message {
pub fn body(&self) -> &str {
match self {
Message::Diagnostic(m) => &m.kind.body,
Message::SyntaxError(m) => &m.message,
Message::SyntaxError(m) => m
.primary_annotation()
.expect("Expected a primary annotation for a ruff diagnostic")
.get_message()
.expect("Expected a message for a ruff diagnostic"),
}
}
@@ -234,27 +246,47 @@ impl Message {
}
/// Returns the filename for the message.
pub fn filename(&self) -> &str {
self.source_file().name()
pub fn filename(&self) -> Cow<'_, str> {
match self {
Message::Diagnostic(m) => Cow::Borrowed(m.file.name()),
Message::SyntaxError(diag) => Cow::Owned(
diag.expect_primary_span()
.expect_ruff_file()
.name()
.to_string(),
),
}
}
/// Computes the start source location for the message.
pub fn compute_start_location(&self) -> LineColumn {
self.source_file()
.to_source_code()
.line_column(self.start())
match self {
Message::Diagnostic(m) => m.file.to_source_code().line_column(m.range.start()),
Message::SyntaxError(diag) => diag
.expect_primary_span()
.expect_ruff_file()
.to_source_code()
.line_column(self.start()),
}
}
/// Computes the end source location for the message.
pub fn compute_end_location(&self) -> LineColumn {
self.source_file().to_source_code().line_column(self.end())
match self {
Message::Diagnostic(m) => m.file.to_source_code().line_column(m.range.end()),
Message::SyntaxError(diag) => diag
.expect_primary_span()
.expect_ruff_file()
.to_source_code()
.line_column(self.end()),
}
}
/// Returns the [`SourceFile`] which the message belongs to.
pub fn source_file(&self) -> &SourceFile {
pub fn source_file(&self) -> SourceFile {
match self {
Message::Diagnostic(m) => &m.file,
Message::SyntaxError(m) => &m.file,
Message::Diagnostic(m) => m.file.clone(),
Message::SyntaxError(m) => m.expect_primary_span().expect_ruff_file().clone(),
}
}
}
@@ -275,7 +307,10 @@ impl Ranged for Message {
fn range(&self) -> TextRange {
match self {
Message::Diagnostic(m) => m.range,
Message::SyntaxError(m) => m.range,
Message::SyntaxError(m) => m
.expect_primary_span()
.range()
.expect("Expected range for ruff span"),
}
}
}
@@ -293,11 +328,11 @@ impl Deref for MessageWithLocation<'_> {
}
}
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<MessageWithLocation>> {
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<String, Vec<MessageWithLocation>> {
let mut grouped_messages = BTreeMap::default();
for message in messages {
grouped_messages
.entry(message.filename())
.entry(message.filename().to_string())
.or_insert_with(Vec::new)
.push(MessageWithLocation {
message,

View File

@@ -18,7 +18,7 @@ impl Emitter for PylintEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let row = if context.is_notebook(message.filename()) {
let row = if context.is_notebook(&message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
OneIndexed::from_zero_indexed(0)
@@ -39,7 +39,7 @@ impl Emitter for PylintEmitter {
writeln!(
writer,
"{path}:{row}: {body}",
path = relativize_path(message.filename()),
path = relativize_path(&*message.filename()),
)?;
}

View File

@@ -57,7 +57,8 @@ impl Serialize for ExpandedMessages<'_> {
}
fn message_to_rdjson_value(message: &Message) -> Value {
let source_code = message.source_file().to_source_code();
let source_file = message.source_file();
let source_code = source_file.to_source_code();
let start_location = source_code.line_column(message.start());
let end_location = source_code.line_column(message.end());

View File

@@ -121,7 +121,7 @@ impl SarifResult {
fn from_message(message: &Message) -> Result<Self> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let path = normalize_path(message.filename());
let path = normalize_path(&*message.filename());
Ok(Self {
rule: message.rule(),
level: "error".to_string(),
@@ -141,7 +141,7 @@ impl SarifResult {
fn from_message(message: &Message) -> Result<Self> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let path = normalize_path(message.filename());
let path = normalize_path(&*message.filename());
Ok(Self {
rule: message.rule(),
level: "error".to_string(),

View File

@@ -73,12 +73,12 @@ impl Emitter for TextEmitter {
write!(
writer,
"{path}{sep}",
path = relativize_path(message.filename()).bold(),
path = relativize_path(&*message.filename()).bold(),
sep = ":".cyan(),
)?;
let start_location = message.compute_start_location();
let notebook_index = context.notebook_index(message.filename());
let notebook_index = context.notebook_index(&message.filename());
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(notebook_index) = notebook_index {
@@ -191,7 +191,8 @@ impl Display for MessageCodeFrame<'_> {
Vec::new()
};
let source_code = self.message.source_file().to_source_code();
let source_file = self.message.source_file();
let source_code = source_file.to_source_code();
let content_start_index = source_code.line_index(self.message.start());
let mut start_index = content_start_index.saturating_sub(2);

View File

@@ -45,7 +45,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 +64,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 +90,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 +101,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 +121,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};
@@ -288,7 +288,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
}
}
["airflow", "operators", "sql", rest] => match *rest {
"BaseSQLOperator" | "BranchSQLOperator" | "SQLTablecheckOperator" => {
"BaseSQLOperator" | "BranchSQLOperator" | "SQLTableCheckOperator" => {
ProviderReplacement::SourceModuleMovedToProvider {
name: (*rest).to_string(),
module: "airflow.providers.common.sql.operators.sql",
@@ -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

@@ -865,10 +865,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(
Airflow3Removal {
deprecated: qualified_name.to_string(),
@@ -876,8 +872,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,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

@@ -242,6 +242,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 +283,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 +291,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

@@ -216,7 +216,7 @@ AIR302_common_sql.py:99:1: AIR302 `airflow.operators.sql.BaseSQLOperator` is mov
99 | BaseSQLOperator()
| ^^^^^^^^^^^^^^^ AIR302
100 | BranchSQLOperator()
101 | SQLTablecheckOperator()
101 | SQLTableCheckOperator()
|
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BaseSQLOperator` instead.
@@ -225,26 +225,26 @@ AIR302_common_sql.py:100:1: AIR302 `airflow.operators.sql.BranchSQLOperator` is
99 | BaseSQLOperator()
100 | BranchSQLOperator()
| ^^^^^^^^^^^^^^^^^ AIR302
101 | SQLTablecheckOperator()
101 | SQLTableCheckOperator()
102 | SQLColumnCheckOperator()
|
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BranchSQLOperator` instead.
AIR302_common_sql.py:101:1: AIR302 `airflow.operators.sql.SQLTablecheckOperator` is moved into `common-sql` provider in Airflow 3.0;
AIR302_common_sql.py:101:1: AIR302 `airflow.operators.sql.SQLTableCheckOperator` is moved into `common-sql` provider in Airflow 3.0;
|
99 | BaseSQLOperator()
100 | BranchSQLOperator()
101 | SQLTablecheckOperator()
101 | SQLTableCheckOperator()
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
102 | SQLColumnCheckOperator()
103 | _convert_to_float_if_possible()
|
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLTablecheckOperator` instead.
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLTableCheckOperator` instead.
AIR302_common_sql.py:102:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator` is moved into `common-sql` provider in Airflow 3.0;
|
100 | BranchSQLOperator()
101 | SQLTablecheckOperator()
101 | SQLTableCheckOperator()
102 | SQLColumnCheckOperator()
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
103 | _convert_to_float_if_possible()
@@ -254,7 +254,7 @@ AIR302_common_sql.py:102:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator
AIR302_common_sql.py:103:1: AIR302 `airflow.operators.sql._convert_to_float_if_possible` is moved into `common-sql` provider in Airflow 3.0;
|
101 | SQLTablecheckOperator()
101 | SQLTableCheckOperator()
102 | SQLColumnCheckOperator()
103 | _convert_to_float_if_possible()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302

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

@@ -186,12 +186,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,39 @@ 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))
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
|
= 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"])

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

@@ -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}")

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