Compare commits

..

41 Commits

Author SHA1 Message Date
Brent Westbrook
cabdd969ec convert to serializable diagnostics 2025-07-21 16:09:52 -04:00
Brent Westbrook
2e5c8b9799 Revert "custom serializers"
This reverts commit e1219bc27c.
2025-07-21 16:09:43 -04:00
Brent Westbrook
e1219bc27c custom serializers 2025-07-21 16:09:27 -04:00
Micha Reiser
926e83323a [ty] Avoid rechecking the entire project when changing the opened files (#19463) 2025-07-21 18:05:03 +02:00
Micha Reiser
5cace28c3e [ty] Add warning for unknown TY_MEMORY_REPORT value (#19465) 2025-07-21 14:29:24 +00:00
github-actions[bot]
3785e13231 [ty] Sync vendored typeshed stubs (#19461)
Co-authored-by: typeshedbot <>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-07-21 14:01:42 +01:00
Alex Waygood
c2380fa0e2 [ty] Extend tuple __len__ and __bool__ special casing to also cover tuple subclasses (#19289)
Co-authored-by: Brent Westbrook
2025-07-21 12:50:46 +00:00
Alex Waygood
4dec44ae49 [ty] bump docstring-adder pin (#19458) 2025-07-21 13:38:40 +01:00
David Peter
b6579eaf04 [ty] Disallow assignment to Final class attributes (#19457)
## Summary

Emit errors for the following assignments:
```py
class C:
    CLASS_LEVEL_CONSTANT: Final[int] = 1

C.CLASS_LEVEL_CONSTANT = 2
C().CLASS_LEVEL_CONSTANT = 2
```

## Test Plan

Updated and new MD tests
2025-07-21 14:27:56 +02:00
renovate[bot]
f063c0e874 Update dependency ruff to v0.12.4 (#19442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:32:09 +02:00
renovate[bot]
6a65734ee3 Update pre-commit hook astral-sh/ruff-pre-commit to v0.12.4 (#19443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:31:55 +02:00
renovate[bot]
00066e094c Update rui314/setup-mold digest to 702b190 (#19441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:31:47 +02:00
renovate[bot]
37a1958374 Update taiki-e/install-action action to v2.56.19 (#19448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:31:18 +02:00
renovate[bot]
2535d791ae Update Rust crate strum_macros to v0.27.2 (#19447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:31:07 +02:00
renovate[bot]
05c4399e7b Update Rust crate strum to v0.27.2 (#19446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:30:51 +02:00
renovate[bot]
b18434b0f6 Update Rust crate rand to v0.9.2 (#19444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:30:07 +02:00
renovate[bot]
17779c9a17 Update Rust crate serde_json to v1.0.141 (#19445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:29:51 +02:00
Dylan
53fc0614da Fix unreachable panic in parser (#19183)
Parsing the (invalid) expression `f"{\t"i}"` caused a panic because the
`TStringMiddle` character was "unreachable" due the way the parser
recovered from the line continuation (it ate the t-string start).

The cause of the issue is as follows: 

The parser begins parsing the f-string and expects to see a list of
objects, essentially alternating between _interpolated elements_ and
ordinary strings. It is happy to see the first left brace, but then
there is a lexical error caused by the line-continuation character. So
instead of the parser seeing a list of elements with just one member, it
sees a list that starts like this:

- Interpolated element with an invalid token, stored as a `Name`
- Something else built from tokens beginning with `TStringStart` and
`TStringMiddle`

When it sees the `TStringStart` error recovery says "that's a list
element I don't know what to do with, let's skip it". When it sees
`TStringMiddle` it says "oh, that looks like the middle of _some
interpolated string_ so let's try to parse it as one of the literal
elements of my `FString`". Unfortunately, the function being used to
parse individual list elements thinks (arguably correctly) that it's not
possible to have a `TStringMiddle` sitting in your `FString`, and hits
`unreachable`.

Two potential ways (among many) to solve this issue are:

1. Allow a `TStringMiddle` as a valid "literal" part of an f-string
during parsing (with the hope/understanding that this would only occur
in an invalid context)
2. Skip the `TStringMiddle` as an "unexpected/invalid list item" in the
same way that we skipped `TStringStart`.

I have opted for the second approach since it seems somehow more morally
correct, even though it loses more information. To implement this, the
recovery context needs to know whether we are in an f-string or t-string
- hence the changes to that enum. As a bonus we get slightly more
specific error messages in some cases.

Closes #18860
2025-07-20 22:04:14 +00:00
Dan Parizher
59249f483b [ruff] Support byte strings (RUF055) (#18926)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

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

Closes #18739

## Test Plan

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

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-07-20 17:58:40 -04:00
Micha Reiser
84e76f4d04 [ty] Avoid second lookup for infer_maybe_standalone_expression (#19439) 2025-07-20 18:22:04 +02:00
UnboundVariable
0acc273286 [ty] Implemented "go to definition" support for import statements (#19428)
This PR extends the "go to declaration" and "go to definition"
functionality to support import statements — both standard imports and
"from" import forms.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-19 11:22:07 -07:00
Micha Reiser
93a9fabb26 [ty] Avoid secondary tree traversal to get call expression for keyword arguments (#19429) 2025-07-19 18:21:27 +02:00
Micha Reiser
98d1811dd1 [ty] Add goto definition to playground (#19425) 2025-07-19 15:44:44 +02:00
Aria Desires
06f9f52e59 [ty] Add support for @warnings.deprecated (#19376)
* [x] basic handling
  * [x] parse and discover `@warnings.deprecated` attributes
  * [x] associate them with function definitions
  * [x] associate them with class definitions
  * [x] add a new "deprecated" diagnostic
* [x] ensure diagnostic is styled appropriately for LSPs
(DiagnosticTag::Deprecated)

* [x] functions
  * [x] fire on calls
  * [x] fire on arbitrary references 
* [x] classes
  * [x] fire on initializers
  * [x] fire on arbitrary references
* [x] methods
  * [x] fire on calls
  * [x] fire on arbitrary references
* [ ] overloads
  * [ ] fire on calls
  * [ ] fire on arbitrary references(??? maybe not ???)
  * [ ] only fire if the actual selected overload is deprecated 

* [ ] dunder desugarring (warn on deprecated `__add__` if `+` is
invoked)
* [ ] alias supression? (don't warn on uses of variables that deprecated
items were assigned to)

* [ ] import logic
  * [x] fire on imports of deprecated items
* [ ] suppress subsequent diagnostics if the import diagnostic fired (is
this handled by alias supression?)
  * [x] fire on all qualified references (`module.mydeprecated`)
  * [x] fire on all references that depend on a `*` import
    


Fixes https://github.com/astral-sh/ty/issues/153
2025-07-18 23:50:29 +00:00
Jack O'Connor
e9a64e5825 [ty] make del x force local resolution of x in the current scope (#19389)
Fixes https://github.com/astral-sh/ty/issues/769.

**Updated:** The preferred approach here is to keep the SemanticIndex
simple (`del` of any name marks that name "bound" in the current scope)
and to move complexity to type inference (free variable resolution stops
when it finds a binding, unless that binding is declared `nonlocal`). As
part of this change, free variable resolution will now union the types
it finds as it walks in enclosing scopes. This approach is still
incomplete, because it doesn't consider inner scopes or sibling scopes,
but it improves the common case.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-07-18 14:58:32 -07:00
UnboundVariable
360eb7005f [ty] Added support for "go to definition" for attribute accesses and keyword arguments (#19417)
This PR builds upon #19371. It addresses a few additional code review
suggestions and adds support for attribute accesses (expressions of the
form `x.y`) and keyword arguments within call expressions.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-18 11:33:57 -07:00
Micha Reiser
630c7a3152 [ty] Reduce number of inline stored definitions per place (#19409) 2025-07-18 18:28:46 +02:00
Ibraheem Ahmed
e6e029a8b7 Update salsa (#19258)
## Summary

Pulls in https://github.com/salsa-rs/salsa/pull/934.
2025-07-18 12:14:28 -04:00
Andrew Gallant
64f9481fd0 [ty] Add caching for submodule completion suggestions (#19408)
This change makes it so we aren't doing a directory traversal every time
we ask for completions from a module. Specifically, submodules that
aren't attributes of their parent module can only be discovered by
looking at the directory tree. But we want to avoid doing a directory
scan unless we think there are changes.

To make this work, this change does a little bit of surgery to
`FileRoot`. Previously, a `FileRoot` was only used for library search
paths. Its revision was bumped whenever a file in that tree was added,
deleted or even modified (to support the discovery of `pth` files and
changes to its contents). This generally seems fine since these are
presumably dependency paths that shouldn't change frequently.

In this change, we add a `FileRoot` for the project. But having the
`FileRoot`'s revision bumped for every change in the project makes
caching based on that `FileRoot` rather ineffective. That is, cache
invalidation will occur too aggressively. To the point that there is
little point in adding caching in the first place. To mitigate this, a
`FileRoot`'s revision is only bumped on a change to a child file's
contents when the `FileRoot` is a `LibrarySearchPath`. Otherwise, we
only bump the revision when a file is created or added.

The effect is that, at least in VS Code, when a new module is added or
removed, this change is picked up and the cache is properly invalidated.
Other LSP clients with worse support for file watching (which seems to
be the case for the CoC vim plugin that I use) don't work as well. Here,
the cache is less likely to be invalidated which might cause completions
to have stale results. Unless there's an obvious way to fix or improve
this, I propose punting on improvements here for now.
2025-07-18 11:54:27 -04:00
Dhruv Manilawala
99d0ac60b4 [ty] Track open files in the server (#19264)
## Summary

This PR updates the server to keep track of open files both system and
virtual files.

This is done by updating the project by adding the file in the open file
set in `didOpen` notification and removing it in `didClose`
notification.

This does mean that for workspace diagnostics, ty will only check open
files because the behavior of different diagnostic builder is to first
check `is_file_open` and only add diagnostics for open files. So, this
required updating the `is_file_open` model to be `should_check_file`
model which validates whether the file needs to be checked based on the
`CheckMode`. If the check mode is open files only then it will check
whether the file is open. If it's all files then it'll return `true` by
default.

Closes: astral-sh/ty#619

## Test Plan

### Before

There are two files in the project: `__init__.py` and `diagnostics.py`.

In the video, I'm demonstrating the old behavior where making changes to
the (open) `diagnostics.py` file results in re-parsing the file:


https://github.com/user-attachments/assets/c2ac0ecd-9c77-42af-a924-c3744b146045

### After

Same setup as above.

In the video, I'm demonstrating the new behavior where making changes to
the (open) `diagnostics.py` file doesn't result in re-parting the file:


https://github.com/user-attachments/assets/7b82fe92-f330-44c7-b527-c841c4545f8f
2025-07-18 19:33:35 +05:30
Andrew Gallant
ba7ed3a6f9 [ty] Use as the "cut" indicator in diagnostic rendering (#19420)
This makes ty match ruff's behavior. Specifically, we want to use `…`
instead of the default `...` because `...` has special significance in
Python.
2025-07-18 07:46:48 -04:00
justin
39b41838f3 [ty] synthesize __setattr__ for frozen dataclasses (#19307)
## Summary

Synthesize a `__setattr__` method with a return type of `Never` for
frozen dataclasses.

https://docs.python.org/3/library/dataclasses.html#frozen-instances

https://docs.python.org/3/library/dataclasses.html#dataclasses.FrozenInstanceError

### Related
https://github.com/astral-sh/ty/issues/111
https://github.com/astral-sh/ruff/pull/17974#discussion_r2108527106
https://github.com/astral-sh/ruff/pull/18347#discussion_r2128174665

## Test Plan

New Markdown tests

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-18 11:35:05 +02:00
UnboundVariable
c7640a433e [ty] Fixed bug in semantic token provider for parameters. (#19418)
This fixes https://github.com/astral-sh/ty/issues/832.

New tests were added to prevent future regressions.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-18 00:02:23 -07:00
Micha Reiser
1765014be3 [ty] Shrink reachability constraints (#19410) 2025-07-18 07:36:18 +02:00
Brent Westbrook
997dc2e7cc Move JUnit rendering to ruff_db (#19370)
Summary
--

This PR moves the JUnit output format to the new rendering
infrastructure. As I
mention in a TODO in the code, there's some code that will be shared
with the
`grouped` output format. Hopefully I'll have that PR up too by the time
this one
is reviewed.

Test Plan
--

Existing tests moved to `ruff_db`

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-17 18:24:13 -04:00
Douglas Creager
4aee0398cb [ty] Show the raw argument type in reveal_type (#19400)
This PR is changes how `reveal_type` determines what type to reveal, in
a way that should be a no-op to most callers.

Previously, we would reveal the type of the first parameter, _after_ all
of the call binding machinery had done its work. This includes inferring
the specialization of a generic function, and then applying that
specialization to all parameter and argument types, which is relevant
since the typeshed definition of `reveal_type` is generic:

```pyi
def reveal_type(obj: _T, /) -> _T: ...
```

Normally this does not matter, since we infer `_T = [arg type]` and
apply that to the parameter type, yielding `[arg type]`. But applying
that specialization also simplifies the argument type, which makes
`reveal_type` less useful as a debugging aid when we want to see the
actual, raw, unsimplified argument type.

With this patch, we now grab the original unmodified argument type and
reveal that instead.

In addition to making the debugging aid example work, this also makes
our `reveal_type` implementation more robust to custom typeshed
definitions, such as

```py
def reveal_type(obj: Any) -> Any: ...
```

(That custom definition is probably not what anyone would want, since
you wouldn't be able to depend on the return type being equivalent to
the argument type, but still)
2025-07-17 16:50:29 -04:00
Brent Westbrook
1fd9103e81 Canonicalize path before filtering (#19407)
## Summary

This came up on
[Discord](https://discord.com/channels/1039017663004942429/1343692072921731082/1395447082520678440)
and also in #19387, but on macOS the tmp directory is a symlink to
`/private/tmp`, which breaks this filter. I'm still not quite sure why
only these tests are affected when we use the `tempdir_filter`
elsewhere, but hopefully this fixes the immediate issue. Just
`tempdir.path().canonicalize()` also worked, but I used `dunce` since
that's what I saw in other tests (I guess it's not _just_ these tests).

Some related links from uv:
-
1b2f212e8b/crates/uv/tests/it/common/mod.rs (L1161-L1178)
-
1b2f212e8b/crates/uv/tests/it/common/mod.rs (L424-L438)
- https://github.com/astral-sh/uv/pull/14290

Thanks to @zanieb for those!

## Test Plan

I tested the `main` branch on my MacBook and reproduced the test
failure, then confirmed that the tests pass after the change. Now to
make sure it passes on Windows, which caused most of the trouble in the
first PR!
2025-07-17 14:02:17 -04:00
Dylan
ee2759b365 Bump 0.12.4 (#19406) 2025-07-17 12:14:01 -05:00
Aria Desires
35f33d9bf5 [ty] publish settings diagnostics (#19335) 2025-07-17 11:57:00 -04:00
chiri
5d78b3117a [flake8-use-pathlib] Add autofix for PTH109 (#19245)
## Summary

Part of #2331

## Test Plan

`cargo nextest run flake8_use_pathlib`
2025-07-17 10:11:43 -04:00
Dhruv Manilawala
c2a05b4825 [ty] Use bitflags for resolved client capabilities (#19393)
## Summary

This PR updates the `ResolvedClientCapabilities` to be represented as
`bitflags`. This allows us to remove the `Arc` as the type becomes copy.

Additionally, this PR also fixed the goto definition and declaration
code to use the `textDocument.definition.linkSupport` and
`textDocument.declaration.linkSupport` client capability.

This PR also removes the unused client capabilities which are
`code_action_deferred_edit_resolution`, `apply_edit`, and
`document_changes` which are all related to auto-fix ability.
2025-07-17 15:31:47 +05:30
744 changed files with 6592 additions and 3554 deletions

View File

@@ -238,13 +238,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -296,13 +296,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-insta
- name: "Run tests"
@@ -325,7 +325,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-nextest
- name: "Run tests"
@@ -381,7 +381,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Build"
run: cargo build --release --locked
@@ -406,7 +406,7 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Build tests"
shell: bash
env:
@@ -903,7 +903,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-codspeed
@@ -936,7 +936,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-codspeed

View File

@@ -38,7 +38,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: Build ruff
# A debug build means the script runs slower once it gets started,

View File

@@ -34,6 +34,10 @@ env:
# and which all three workers push to.
UPSTREAM_BRANCH: typeshedbot/sync-typeshed
# The path to the directory that contains the vendored typeshed stubs,
# relative to the root of the Ruff repository.
VENDORED_TYPESHED: crates/ty_vendored/vendor/typeshed
jobs:
# Sync typeshed stubs, and sync all docstrings available on Linux.
# Push the changes to a new branch on the upstream repository.
@@ -64,20 +68,20 @@ jobs:
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Sync typeshed stubs
run: |
rm -rf ruff/crates/ty_vendored/vendor/typeshed
mkdir ruff/crates/ty_vendored/vendor/typeshed
cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed
cp typeshed/LICENSE ruff/crates/ty_vendored/vendor/typeshed
rm -rf "ruff/${VENDORED_TYPESHED}"
mkdir "ruff/${VENDORED_TYPESHED}"
cp typeshed/README.md "ruff/${VENDORED_TYPESHED}"
cp typeshed/LICENSE "ruff/${VENDORED_TYPESHED}"
# The pyproject.toml file is needed by a later job for the black configuration.
# It's deleted before creating the PR.
cp typeshed/pyproject.toml ruff/crates/ty_vendored/vendor/typeshed
cp typeshed/pyproject.toml "ruff/${VENDORED_TYPESHED}"
cp -r typeshed/stdlib ruff/crates/ty_vendored/vendor/typeshed/stdlib
rm -rf ruff/crates/ty_vendored/vendor/typeshed/stdlib/@tests
git -C typeshed rev-parse HEAD > ruff/crates/ty_vendored/vendor/typeshed/source_commit.txt
cp -r typeshed/stdlib "ruff/${VENDORED_TYPESHED}/stdlib"
rm -rf "ruff/${VENDORED_TYPESHED}/stdlib/@tests"
git -C typeshed rev-parse HEAD > "ruff/${VENDORED_TYPESHED}/source_commit.txt"
cd ruff
git checkout -b typeshedbot/sync-typeshed
git checkout -b "${UPSTREAM_BRANCH}"
git add .
git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)" --allow-empty
- name: Sync Linux docstrings
@@ -167,17 +171,17 @@ jobs:
# consistent with the other typeshed stubs around them.
# Typeshed formats code using black in their CI, so we just invoke
# black on the stubs the same way that typeshed does.
uvx black crates/ty_vendored/vendor/typeshed/stdlib --config crates/ty_vendored/vendor/typeshed/pyproject.toml || true
uvx black "${VENDORED_TYPESHED}/stdlib" --config "${VENDORED_TYPESHED}/pyproject.toml" || true
git commit -am "Format codemodded docstrings" --allow-empty
rm crates/ty_vendored/vendor/typeshed/pyproject.toml
rm "${VENDORED_TYPESHED}/pyproject.toml"
git commit -am "Remove pyproject.toml file"
git push
- name: Create a PR
if: ${{ success() }}
run: |
gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr list --repo "${GITHUB_REPOSITORY}" --head "${UPSTREAM_BRANCH}" --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"
create-issue-on-failure:

View File

@@ -81,7 +81,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.3
rev: v0.12.4
hooks:
- id: ruff-format
- id: ruff

View File

@@ -1,5 +1,36 @@
# Changelog
## 0.12.4
### Preview features
- \[`flake8-type-checking`, `pyupgrade`, `ruff`\] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) ([#19100](https://github.com/astral-sh/ruff/pull/19100))
- \[`flake8-use-pathlib`\] Add autofix for `PTH109` ([#19245](https://github.com/astral-sh/ruff/pull/19245))
- \[`pylint`\] Detect indirect `pathlib.Path` usages for `unspecified-encoding` (`PLW1514`) ([#19304](https://github.com/astral-sh/ruff/pull/19304))
### Bug fixes
- \[`flake8-bugbear`\] Fix `B017` false negatives for keyword exception arguments ([#19217](https://github.com/astral-sh/ruff/pull/19217))
- \[`flake8-use-pathlib`\] Fix false negative on direct `Path()` instantiation (`PTH210`) ([#19388](https://github.com/astral-sh/ruff/pull/19388))
- \[`flake8-django`\] Fix `DJ008` false positive for abstract models with type-annotated `abstract` field ([#19221](https://github.com/astral-sh/ruff/pull/19221))
- \[`isort`\] Fix `I002` import insertion after docstring with multiple string statements ([#19222](https://github.com/astral-sh/ruff/pull/19222))
- \[`isort`\] Treat form feed as valid whitespace before a semicolon ([#19343](https://github.com/astral-sh/ruff/pull/19343))
- \[`pydoclint`\] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) ([#19246](https://github.com/astral-sh/ruff/pull/19246))
- \[`refurb`\] `FURB164` fix should validate arguments and should usually be marked unsafe ([#19136](https://github.com/astral-sh/ruff/pull/19136))
### Rule changes
- \[`flake8-use-pathlib`\] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 ([#19331](https://github.com/astral-sh/ruff/pull/19331))
- \[`pep8_naming`\] Avoid false positives on standard library functions with uppercase names (`N802`) ([#18907](https://github.com/astral-sh/ruff/pull/18907))
- \[`pycodestyle`\] Handle brace escapes for t-strings in logical lines ([#19358](https://github.com/astral-sh/ruff/pull/19358))
- \[`pylint`\] Extend invalid string character rules to include t-strings ([#19355](https://github.com/astral-sh/ruff/pull/19355))
- \[`ruff`\] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ ([#19333](https://github.com/astral-sh/ruff/pull/19333))
### Documentation
- \[`flake8-type-checking`\] Make `TC010` docs example more realistic ([#19356](https://github.com/astral-sh/ruff/pull/19356))
- Make more documentation examples error out-of-the-box ([#19288](https://github.com/astral-sh/ruff/pull/19288),[#19272](https://github.com/astral-sh/ruff/pull/19272),[#19291](https://github.com/astral-sh/ruff/pull/19291),[#19296](https://github.com/astral-sh/ruff/pull/19296),[#19292](https://github.com/astral-sh/ruff/pull/19292),[#19295](https://github.com/astral-sh/ruff/pull/19295),[#19297](https://github.com/astral-sh/ruff/pull/19297),[#19309](https://github.com/astral-sh/ruff/pull/19309))
## 0.12.3
### Preview features

67
Cargo.lock generated
View File

@@ -1557,6 +1557,15 @@ dependencies = [
"memoffset",
]
[[package]]
name = "inventory"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83"
dependencies = [
"rustversion",
]
[[package]]
name = "is-docker"
version = "0.2.0"
@@ -2123,16 +2132,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "papaya"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f"
dependencies = [
"equivalent",
"seize",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -2562,9 +2561,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
@@ -2711,7 +2710,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.3"
version = "0.12.4"
dependencies = [
"anyhow",
"argfile",
@@ -2839,6 +2838,7 @@ dependencies = [
"insta",
"matchit",
"path-slash",
"quick-junit",
"ruff_annotate_snippets",
"ruff_cache",
"ruff_diagnostics",
@@ -2962,7 +2962,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.3"
version = "0.12.4"
dependencies = [
"aho-corasick",
"anyhow",
@@ -2987,7 +2987,6 @@ dependencies = [
"pathdiff",
"pep440_rs",
"pyproject-toml",
"quick-junit",
"regex",
"ruff_annotate_snippets",
"ruff_cache",
@@ -3042,7 +3041,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
"rand 0.9.1",
"rand 0.9.2",
"ruff_diagnostics",
"ruff_source_file",
"ruff_text_size",
@@ -3295,7 +3294,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.3"
version = "0.12.4"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3409,7 +3408,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b"
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
dependencies = [
"boxcar",
"compact_str",
@@ -3419,7 +3418,7 @@ dependencies = [
"hashlink",
"indexmap",
"intrusive-collections",
"papaya",
"inventory",
"parking_lot",
"portable-atomic",
"rayon",
@@ -3434,12 +3433,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b"
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
[[package]]
name = "salsa-macros"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b"
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
dependencies = [
"proc-macro2",
"quote",
@@ -3492,16 +3491,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "seize"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4b8d813387d566f627f3ea1b914c068aac94c40ae27ec43f5f33bde65abefe7"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "serde"
version = "1.0.219"
@@ -3546,9 +3535,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
dependencies = [
"itoa",
"memchr",
@@ -3738,23 +3727,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
@@ -4300,6 +4288,7 @@ name = "ty_server"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"crossbeam",
"jod-thread",
"libc",
@@ -4574,7 +4563,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"rand 0.9.1",
"rand 0.9.2",
"uuid-macro-internal",
"wasm-bindgen",
]

View File

@@ -138,7 +138,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "fc00eba89e5dcaa5edba51c41aa5f309b5cb126b" }
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "dba66f1a37acca014c2402f231ed5b361bd7d8fe" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -150,7 +150,7 @@ serde_with = { version = "3.6.0", default-features = false, features = [
] }
shellexpand = { version = "3.0.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.2" }
smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new"] }
snapbox = { version = "0.6.0", features = [
"diff",
"term-svg",

View File

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

View File

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

View File

@@ -18,14 +18,12 @@ use rustc_hash::FxHashMap;
use tempfile::NamedTempFile;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::diagnostic::{Diagnostic, SerializableDiagnostics};
use ruff_diagnostics::Fix;
use ruff_linter::message::create_lint_diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::{VERSION, warn_user};
use ruff_macros::CacheKey;
use ruff_notebook::NotebookIndex;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::Settings;
use ruff_workspace::resolver::Resolver;
@@ -345,22 +343,7 @@ impl FileCache {
let diagnostics = if lint.messages.is_empty() {
Vec::new()
} else {
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
lint.messages
.iter()
.map(|msg| {
create_lint_diagnostic(
&msg.body,
msg.suggestion.as_ref(),
msg.range,
msg.fix.clone(),
msg.parent,
file.clone(),
msg.noqa_offset,
msg.rule,
)
})
.collect()
lint.messages.to_diagnostics()
};
let notebook_indexes = if let Some(notebook_index) = lint.notebook_index.as_ref() {
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
@@ -415,7 +398,8 @@ pub(crate) struct LintCacheData {
/// Imports made.
// pub(super) imports: ImportMap,
/// Diagnostic messages.
pub(super) messages: Vec<CacheMessage>,
#[bincode(with_serde)]
pub(super) messages: SerializableDiagnostics,
/// Source code of the file.
///
/// # Notes
@@ -438,30 +422,7 @@ impl LintCacheData {
String::new() // No messages, no need to keep the source!
};
let messages = diagnostics
.iter()
// Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
// this also serves to filter them out, but we shouldn't be caching files with syntax
// errors anyway.
.filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
.map(|(rule, msg)| {
// Make sure that all message use the same source file.
assert_eq!(
msg.expect_ruff_source_file(),
diagnostics.first().unwrap().expect_ruff_source_file(),
"message uses a different source file"
);
CacheMessage {
rule,
body: msg.body().to_string(),
suggestion: msg.suggestion().map(ToString::to_string),
range: msg.expect_range(),
parent: msg.parent(),
fix: msg.fix().cloned(),
noqa_offset: msg.noqa_offset(),
}
})
.collect();
let messages = SerializableDiagnostics::new(diagnostics);
Self {
messages,

View File

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

View File

@@ -5718,8 +5718,11 @@ match 42: # invalid-syntax
let snapshot = format!("output_format_{output_format}");
let project_dir = dunce::canonicalize(tempdir.path())?;
insta::with_settings!({
filters => vec![
(tempdir_filter(&project_dir).as_str(), "[TMP]/"),
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
(r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#),
(ruff_linter::VERSION, "[VERSION]"),

View File

@@ -25,7 +25,7 @@ exit_code: 1
<testcase name="org.ruff.F821" classname="[TMP]/input" line="2" column="5">
<failure message="Undefined name `y`">line 2, col 5, Undefined name `y`</failure>
</testcase>
<testcase name="org.ruff" classname="[TMP]/input" line="3" column="1">
<testcase name="org.ruff.invalid-syntax" classname="[TMP]/input" line="3" column="1">
<failure message="SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">line 3, col 1, SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)</failure>
</testcase>
</testsuite>

View File

@@ -18,7 +18,7 @@ use ruff_python_ast::PythonVersion;
use ty_project::metadata::options::{EnvironmentOptions, Options};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ChangedKind};
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata};
struct Case {
db: ProjectDatabase,
@@ -102,6 +102,7 @@ fn setup_tomllib_case() -> Case {
let re = re.unwrap();
db.set_check_mode(CheckMode::OpenFiles);
db.project().set_open_files(&mut db, tomllib_files);
let re_path = re.path(&db).as_system_path().unwrap().to_owned();
@@ -237,6 +238,7 @@ fn setup_micro_case(code: &str) -> Case {
let mut db = ProjectDatabase::new(metadata, system).unwrap();
let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap();
db.set_check_mode(CheckMode::OpenFiles);
db.project()
.set_open_files(&mut db, FxHashSet::from_iter([file]));
@@ -525,14 +527,21 @@ impl<'a> ProjectBenchmark<'a> {
#[track_caller]
fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) {
fn check_project(db: &mut ProjectDatabase, max_diagnostics: usize) {
fn check_project(db: &mut ProjectDatabase, project_name: &str, max_diagnostics: usize) {
let result = db.check();
let diagnostics = result.len();
assert!(
diagnostics <= max_diagnostics,
"Expected <={max_diagnostics} diagnostics but got {diagnostics}"
);
if diagnostics > max_diagnostics {
let details = result
.into_iter()
.map(|diagnostic| diagnostic.concise_message().to_string())
.collect::<Vec<_>>()
.join("\n ");
assert!(
diagnostics <= max_diagnostics,
"{project_name}: Expected <={max_diagnostics} diagnostics but got {diagnostics}:\n {details}",
);
}
}
setup_rayon();
@@ -542,7 +551,7 @@ fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) {
group.bench_function(benchmark.project.config.name, |b| {
b.iter_batched_ref(
|| benchmark.setup_iteration(),
|db| check_project(db, benchmark.max_diagnostics),
|db| check_project(db, benchmark.project.config.name, benchmark.max_diagnostics),
BatchSize::SmallInput,
);
});
@@ -610,7 +619,7 @@ fn datetype(criterion: &mut Criterion) {
max_dep_date: "2025-07-04",
python_version: PythonVersion::PY313,
},
0,
2,
);
bench_project(&benchmark, criterion);

View File

@@ -34,6 +34,7 @@ glob = { workspace = true }
ignore = { workspace = true, optional = true }
matchit = { workspace = true }
path-slash = { workspace = true }
quick-junit = { workspace = true, optional = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
@@ -56,6 +57,7 @@ tempfile = { workspace = true }
[features]
cache = ["ruff_cache"]
junit = ["dep:quick-junit"]
os = ["ignore", "dep:etcetera"]
serde = ["camino/serde1", "dep:serde", "dep:serde_json", "ruff_diagnostics/serde"]
# Exposes testing utilities.

View File

@@ -9,6 +9,9 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
use crate::{Db, files::File};
#[cfg(feature = "serde")]
pub use serde_diagnostics::SerializableDiagnostics;
mod render;
mod stylesheet;
@@ -790,6 +793,7 @@ impl Annotation {
/// These tags are used to provide additional information about the annotation.
/// and are passed through to the language server protocol.
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DiagnosticTag {
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
Unnecessary,
@@ -804,6 +808,7 @@ pub enum DiagnosticTag {
///
/// Rules use kebab case, e.g. `no-foo`.
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LintName(&'static str);
impl LintName {
@@ -844,6 +849,7 @@ impl PartialEq<&str> for LintName {
/// Uniquely identifies the kind of a diagnostic.
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DiagnosticId {
Panic,
@@ -1141,6 +1147,7 @@ impl From<crate::files::FileRange> for Span {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity {
Info,
Warning,
@@ -1282,6 +1289,9 @@ pub enum DiagnosticFormat {
Rdjson,
/// Print diagnostics in the format emitted by Pylint.
Pylint,
/// Print diagnostics in the format expected by JUnit.
#[cfg(feature = "junit")]
Junit,
}
/// A representation of the kinds of messages inside a diagnostic.
@@ -1341,6 +1351,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
/// a blanket trait implementation for `IntoDiagnosticMessage` for
/// anything that implements `std::fmt::Display`.
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DiagnosticMessage(Box<str>);
impl DiagnosticMessage {
@@ -1404,7 +1415,11 @@ impl<T: std::fmt::Display> IntoDiagnosticMessage for T {
///
/// For Ruff rules this means the noqa code.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(transparent)
)]
pub struct SecondaryCode(String);
impl SecondaryCode {
@@ -1449,3 +1464,205 @@ impl From<&SecondaryCode> for SecondaryCode {
value.clone()
}
}
#[cfg(feature = "serde")]
mod serde_diagnostics {
use std::sync::Arc;
use ruff_diagnostics::Fix;
use ruff_source_file::{SourceFile, SourceFileBuilder};
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use super::{
Annotation, Diagnostic, DiagnosticId, DiagnosticInner, DiagnosticMessage, DiagnosticTag,
LintName, SecondaryCode, Severity, Span, SubDiagnostic, SubDiagnosticInner, UnifiedFile,
};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct SerializableDiagnostics {
source_files: FxHashMap<String, String>,
diagnostics: Vec<SerializableDiagnostic>,
}
impl SerializableDiagnostics {
pub fn new(diagnostics: &[Diagnostic]) -> Self {
let mut source_files = FxHashMap::default();
let mut serializable_diagnostics = Vec::with_capacity(diagnostics.len());
for diagnostic in diagnostics {
let mut subs = Vec::with_capacity(diagnostic.inner.subs.len());
for sub in &diagnostic.inner.subs {
subs.push(SerializableSubDiagnostic {
severity: sub.inner.severity,
message: sub.inner.message.clone(),
annotations: serializable_annotations(
&mut source_files,
&sub.inner.annotations,
),
});
}
serializable_diagnostics.push(SerializableDiagnostic {
id: diagnostic.inner.id,
severity: diagnostic.inner.severity,
message: diagnostic.inner.message.clone(),
annotations: serializable_annotations(
&mut source_files,
&diagnostic.inner.annotations,
),
subs,
fix: diagnostic.inner.fix.clone(),
parent: diagnostic.inner.parent,
noqa_offset: diagnostic.inner.noqa_offset,
secondary_code: diagnostic.inner.secondary_code.clone(),
});
}
Self {
source_files,
diagnostics: serializable_diagnostics,
}
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn to_diagnostics(&self) -> Vec<Diagnostic> {
let source_files: FxHashMap<&str, SourceFile> = self
.source_files
.iter()
.map(|(name, contents)| {
(
name.as_str(),
SourceFileBuilder::new(name.clone(), contents.clone()).finish(),
)
})
.collect();
self.diagnostics
.iter()
.map(|diag| Diagnostic {
inner: Arc::new(DiagnosticInner {
id: diag.id,
severity: diag.severity,
message: diag.message.clone(),
annotations: annotations(&source_files, &diag.annotations),
subs: subdiagnostics(&source_files, &diag.subs),
fix: diag.fix.clone(),
parent: diag.parent,
noqa_offset: diag.noqa_offset,
secondary_code: diag.secondary_code.clone(),
}),
})
.collect()
}
}
fn serializable_annotations(
source_files: &mut FxHashMap<String, String>,
annotations: &[Annotation],
) -> Vec<SerializableAnnotation> {
let mut serializable_annotations = Vec::with_capacity(annotations.len());
for annotation in annotations {
let file = annotation.span.expect_ruff_file();
source_files.insert(file.name().to_string(), file.source_text().to_string());
serializable_annotations.push(SerializableAnnotation {
span: SerializableSpan {
name: file.name().to_string(),
range: annotation.span.range,
},
message: annotation.message.clone(),
is_primary: annotation.is_primary,
tags: annotation.tags.clone(),
});
}
serializable_annotations
}
fn annotations(
source_files: &FxHashMap<&str, SourceFile>,
serializable_annotations: &[SerializableAnnotation],
) -> Vec<Annotation> {
serializable_annotations
.iter()
.map(|ann| {
let span = Span {
file: UnifiedFile::Ruff(
source_files
.get(ann.span.name.as_str())
.expect("Expected source file in cache")
.clone(),
),
range: ann.span.range,
};
Annotation {
span,
message: ann.message.clone(),
is_primary: ann.is_primary,
tags: ann.tags.clone(),
}
})
.collect()
}
fn subdiagnostics(
source_files: &FxHashMap<&str, SourceFile>,
serializable_annotations: &[SerializableSubDiagnostic],
) -> Vec<SubDiagnostic> {
serializable_annotations
.iter()
.map(|sub| SubDiagnostic {
inner: Box::new(SubDiagnosticInner {
severity: sub.severity,
message: sub.message.clone(),
annotations: annotations(source_files, &sub.annotations),
}),
})
.collect()
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SerializableDiagnostic {
id: DiagnosticId,
severity: Severity,
message: DiagnosticMessage,
annotations: Vec<SerializableAnnotation>,
subs: Vec<SerializableSubDiagnostic>,
fix: Option<Fix>,
parent: Option<TextSize>,
noqa_offset: Option<TextSize>,
secondary_code: Option<SecondaryCode>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SerializableAnnotation {
span: SerializableSpan,
message: Option<DiagnosticMessage>,
is_primary: bool,
tags: Vec<DiagnosticTag>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SerializableSubDiagnostic {
severity: Severity,
message: DiagnosticMessage,
annotations: Vec<SerializableAnnotation>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SerializableSpan {
name: String,
range: Option<TextRange>,
}
impl<'de> Deserialize<'de> for LintName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?.into_boxed_str();
Ok(LintName::of(Box::leak(s)))
}
}
}

View File

@@ -30,6 +30,8 @@ mod azure;
mod json;
#[cfg(feature = "serde")]
mod json_lines;
#[cfg(feature = "junit")]
mod junit;
mod pylint;
#[cfg(feature = "serde")]
mod rdjson;
@@ -156,7 +158,8 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
AnnotateRenderer::styled()
} else {
AnnotateRenderer::plain()
};
}
.cut_indicator("");
renderer = renderer
.error(stylesheet.error)
@@ -196,6 +199,10 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
DiagnosticFormat::Pylint => {
PylintRenderer::new(self.resolver).render(f, self.diagnostics)?;
}
#[cfg(feature = "junit")]
DiagnosticFormat::Junit => {
junit::JunitRenderer::new(self.resolver).render(f, self.diagnostics)?;
}
}
Ok(())

View File

@@ -0,0 +1,195 @@
use std::{collections::BTreeMap, ops::Deref, path::Path};
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString};
use ruff_source_file::LineColumn;
use crate::diagnostic::{Diagnostic, SecondaryCode, render::FileResolver};
/// A renderer for diagnostics in the [JUnit] format.
///
/// See [`junit.xsd`] for the specification in the JUnit repository and an annotated [version]
/// linked from the [`quick_junit`] docs.
///
/// [JUnit]: https://junit.org/
/// [`junit.xsd`]: https://github.com/junit-team/junit-framework/blob/2870b7d8fd5bf7c1efe489d3991d3ed3900e82bb/platform-tests/src/test/resources/jenkins-junit.xsd
/// [version]: https://llg.cubic.org/docs/junit/
/// [`quick_junit`]: https://docs.rs/quick-junit/latest/quick_junit/
pub struct JunitRenderer<'a> {
resolver: &'a dyn FileResolver,
}
impl<'a> JunitRenderer<'a> {
pub fn new(resolver: &'a dyn FileResolver) -> Self {
Self { resolver }
}
pub(super) fn render(
&self,
f: &mut std::fmt::Formatter,
diagnostics: &[Diagnostic],
) -> std::fmt::Result {
let mut report = Report::new("ruff");
if diagnostics.is_empty() {
let mut test_suite = TestSuite::new("ruff");
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
case.set_classname("ruff");
test_suite.add_test_case(case);
report.add_test_suite(test_suite);
} else {
for (filename, diagnostics) in group_diagnostics_by_filename(diagnostics, self.resolver)
{
let mut test_suite = TestSuite::new(filename);
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
let classname = Path::new(filename).with_extension("");
for diagnostic in diagnostics {
let DiagnosticWithLocation {
diagnostic,
start_location: location,
} = diagnostic;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(diagnostic.body());
if let Some(location) = location {
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.line,
col = location.column,
body = diagnostic.body()
));
} else {
status.set_description(diagnostic.body());
}
let code = diagnostic
.secondary_code()
.map_or_else(|| diagnostic.name(), SecondaryCode::as_str);
let mut case = TestCase::new(format!("org.ruff.{code}"), status);
case.set_classname(classname.to_str().unwrap());
if let Some(location) = location {
case.extra.insert(
XmlString::new("line"),
XmlString::new(location.line.to_string()),
);
case.extra.insert(
XmlString::new("column"),
XmlString::new(location.column.to_string()),
);
}
test_suite.add_test_case(case);
}
report.add_test_suite(test_suite);
}
}
let adapter = FmtAdapter { fmt: f };
report.serialize(adapter).map_err(|_| std::fmt::Error)
}
}
// TODO(brent) this and `group_diagnostics_by_filename` are also used by the `grouped` output
// format. I think they'd make more sense in that file, but I started here first. I'll move them to
// that module when adding the `grouped` output format.
struct DiagnosticWithLocation<'a> {
diagnostic: &'a Diagnostic,
start_location: Option<LineColumn>,
}
impl Deref for DiagnosticWithLocation<'_> {
type Target = Diagnostic;
fn deref(&self) -> &Self::Target {
self.diagnostic
}
}
fn group_diagnostics_by_filename<'a>(
diagnostics: &'a [Diagnostic],
resolver: &'a dyn FileResolver,
) -> BTreeMap<&'a str, Vec<DiagnosticWithLocation<'a>>> {
let mut grouped_diagnostics = BTreeMap::default();
for diagnostic in diagnostics {
let (filename, start_location) = diagnostic
.primary_span_ref()
.map(|span| {
let file = span.file();
let start_location =
span.range()
.filter(|_| !resolver.is_notebook(file))
.map(|range| {
file.diagnostic_source(resolver)
.as_source_code()
.line_column(range.start())
});
(span.file().path(resolver), start_location)
})
.unwrap_or_default();
grouped_diagnostics
.entry(filename)
.or_insert_with(Vec::new)
.push(DiagnosticWithLocation {
diagnostic,
start_location,
});
}
grouped_diagnostics
}
struct FmtAdapter<'a> {
fmt: &'a mut dyn std::fmt::Write,
}
impl std::io::Write for FmtAdapter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.fmt
.write_str(std::str::from_utf8(buf).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid UTF-8 in JUnit report",
)
})?)
.map_err(std::io::Error::other)?;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
self.fmt.write_fmt(args).map_err(std::io::Error::other)
}
}
#[cfg(test)]
mod tests {
use crate::diagnostic::{
DiagnosticFormat,
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
};
#[test]
fn output() {
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Junit);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
}
#[test]
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Junit);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
}
}

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_linter/src/message/junit.rs
expression: content
snapshot_kind: text
source: crates/ruff_db/src/diagnostic/render/junit.rs
expression: env.render_diagnostics(&diagnostics)
---
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="3" failures="3" errors="0">

View File

@@ -1,15 +1,14 @@
---
source: crates/ruff_linter/src/message/junit.rs
expression: content
snapshot_kind: text
source: crates/ruff_db/src/diagnostic/render/junit.rs
expression: env.render_diagnostics(&diagnostics)
---
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="2" failures="2" errors="0">
<testsuite name="syntax_errors.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
<testcase name="org.ruff" classname="syntax_errors" line="1" column="15">
<testcase name="org.ruff.invalid-syntax" classname="syntax_errors" line="1" column="15">
<failure message="SyntaxError: Expected one or more symbol names after import">line 1, col 15, SyntaxError: Expected one or more symbol names after import</failure>
</testcase>
<testcase name="org.ruff" classname="syntax_errors" line="3" column="12">
<testcase name="org.ruff.invalid-syntax" classname="syntax_errors" line="3" column="12">
<failure message="SyntaxError: Expected &apos;)&apos;, found newline">line 3, col 12, SyntaxError: Expected &apos;)&apos;, found newline</failure>
</testcase>
</testsuite>

View File

@@ -232,7 +232,7 @@ impl Files {
let roots = inner.roots.read().unwrap();
for root in roots.all() {
if root.path(db).starts_with(&path) {
if path.starts_with(root.path(db)) {
root.set_revision(db).to(FileRevision::now());
}
}
@@ -375,12 +375,25 @@ impl File {
}
/// Refreshes the file metadata by querying the file system if needed.
///
/// This also "touches" the file root associated with the given path.
/// This means that any Salsa queries that depend on the corresponding
/// root's revision will become invalidated.
pub fn sync_path(db: &mut dyn Db, path: &SystemPath) {
let absolute = SystemPath::absolute(path, db.system().current_directory());
Files::touch_root(db, &absolute);
Self::sync_system_path(db, &absolute, None);
}
/// Refreshes *only* the file metadata by querying the file system if needed.
///
/// This specifically does not touch any file root associated with the
/// given file path.
pub fn sync_path_only(db: &mut dyn Db, path: &SystemPath) {
let absolute = SystemPath::absolute(path, db.system().current_directory());
Self::sync_system_path(db, &absolute, None);
}
/// Increments the revision for the virtual file at `path`.
pub fn sync_virtual_path(db: &mut dyn Db, path: &SystemVirtualPath) {
if let Some(virtual_file) = db.files().try_virtual_file(path) {
@@ -486,7 +499,7 @@ impl fmt::Debug for File {
///
/// This is a wrapper around a [`File`] that provides additional methods to interact with a virtual
/// file.
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub struct VirtualFile(File);
impl VirtualFile {

View File

@@ -23,7 +23,7 @@ pub struct FileRoot {
pub path: SystemPathBuf,
/// The kind of the root at the time of its creation.
kind_at_time_of_creation: FileRootKind,
pub kind_at_time_of_creation: FileRootKind,
/// A revision that changes when the contents of the source root change.
///

View File

@@ -87,7 +87,7 @@ impl SourceDb for ModuleDb {
#[salsa::db]
impl Db for ModuleDb {
fn is_file_open(&self, file: File) -> bool {
fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path()
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.12.3"
version = "0.12.4"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -15,7 +15,7 @@ license = { workspace = true }
[dependencies]
ruff_annotate_snippets = { workspace = true }
ruff_cache = { workspace = true }
ruff_db = { workspace = true, features = ["serde"] }
ruff_db = { workspace = true, features = ["junit", "serde"] }
ruff_diagnostics = { workspace = true, features = ["serde"] }
ruff_notebook = { workspace = true }
ruff_macros = { workspace = true }
@@ -55,7 +55,6 @@ path-absolutize = { workspace = true, features = [
pathdiff = { workspace = true }
pep440_rs = { workspace = true }
pyproject-toml = { workspace = true }
quick-junit = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }

View File

@@ -104,3 +104,6 @@ os.chmod(x)
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)
os.getcwd()
os.getcwdb()

View File

@@ -79,3 +79,8 @@ def in_type_def():
from typing import cast
a = 'int'
cast('f"{a}"','11')
# Regression test for parser bug
# https://github.com/astral-sh/ruff/issues/18860
def fuzz_bug():
c('{\t"i}')

View File

@@ -0,0 +1,24 @@
import re
b_src = b"abc"
# Should be replaced with `b_src.replace(rb"x", b"y")`
re.sub(rb"x", b"y", b_src)
# Should be replaced with `b_src.startswith(rb"abc")`
if re.match(rb"abc", b_src):
pass
# Should be replaced with `rb"x" in b_src`
if re.search(rb"x", b_src):
pass
# Should be replaced with `b_src.split(rb"abc")`
re.split(rb"abc", b_src)
# Patterns containing metacharacters should NOT be replaced
re.sub(rb"ab[c]", b"", b_src)
re.match(rb"ab[c]", b_src)
re.search(rb"ab[c]", b_src)
re.fullmatch(rb"ab[c]", b_src)
re.split(rb"ab[c]", b_src)

View File

@@ -1044,7 +1044,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::OsMakedirs,
Rule::OsRename,
Rule::OsReplace,
Rule::OsGetcwd,
Rule::OsStat,
Rule::OsPathJoin,
Rule::OsPathSamefile,
@@ -1110,6 +1109,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsReadlink) {
flake8_use_pathlib::rules::os_readlink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsGetcwd) {
flake8_use_pathlib::rules::os_getcwd(checker, call, segments);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(
checker, call, segments,

View File

@@ -928,7 +928,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
(Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsGetcwd),
(Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsGetcwd),
(Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExists),
(Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExpanduser),
(Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsdir),

View File

@@ -1,117 +0,0 @@
use std::io::Write;
use std::path::Path;
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString};
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::LineColumn;
use crate::message::{Emitter, EmitterContext, MessageWithLocation, group_diagnostics_by_filename};
#[derive(Default)]
pub struct JunitEmitter;
impl Emitter for JunitEmitter {
fn emit(
&mut self,
writer: &mut dyn Write,
diagnostics: &[Diagnostic],
context: &EmitterContext,
) -> anyhow::Result<()> {
let mut report = Report::new("ruff");
if diagnostics.is_empty() {
let mut test_suite = TestSuite::new("ruff");
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
case.set_classname("ruff");
test_suite.add_test_case(case);
report.add_test_suite(test_suite);
} else {
for (filename, messages) in group_diagnostics_by_filename(diagnostics) {
let mut test_suite = TestSuite::new(&filename);
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
for message in messages {
let MessageWithLocation {
message,
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.body());
let location = if context.is_notebook(&message.expect_ruff_filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()
} else {
start_location
};
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.line,
col = location.column,
body = message.body()
));
let mut case = TestCase::new(
if let Some(code) = message.secondary_code() {
format!("org.ruff.{code}")
} else {
"org.ruff".to_string()
},
status,
);
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());
case.extra.insert(
XmlString::new("line"),
XmlString::new(location.line.to_string()),
);
case.extra.insert(
XmlString::new("column"),
XmlString::new(location.column.to_string()),
);
test_suite.add_test_case(case);
}
report.add_test_suite(test_suite);
}
}
report.serialize(writer)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::JunitEmitter;
use crate::message::tests::{
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
};
#[test]
fn output() {
let mut emitter = JunitEmitter;
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = JunitEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
assert_snapshot!(content);
}
}

View File

@@ -14,7 +14,6 @@ use ruff_db::files::File;
pub use github::GithubEmitter;
pub use gitlab::GitlabEmitter;
pub use grouped::GroupedEmitter;
pub use junit::JunitEmitter;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{LineColumn, SourceFile};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -28,7 +27,6 @@ mod diff;
mod github;
mod gitlab;
mod grouped;
mod junit;
mod sarif;
mod text;

View File

@@ -134,6 +134,11 @@ pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) ->
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19245
pub(crate) const fn is_fix_os_getcwd_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11436
// https://github.com/astral-sh/ruff/pull/11168
pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool {

View File

@@ -1,5 +1,6 @@
pub(crate) use glob_rule::*;
pub(crate) use invalid_pathlib_with_suffix::*;
pub(crate) use os_getcwd::*;
pub(crate) use os_path_abspath::*;
pub(crate) use os_path_basename::*;
pub(crate) use os_path_dirname::*;
@@ -23,6 +24,7 @@ pub(crate) use replaceable_by_pathlib::*;
mod glob_rule;
mod invalid_pathlib_with_suffix;
mod os_getcwd;
mod os_path_abspath;
mod os_path_basename;
mod os_path_dirname;

View File

@@ -0,0 +1,100 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_getcwd_enabled;
use crate::{FixAvailability, Violation};
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
/// ## What it does
/// Checks for uses of `os.getcwd` and `os.getcwdb`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.cwd()` can improve readability over the `os`
/// module's counterparts (e.g., `os.getcwd()`).
///
/// ## Examples
/// ```python
/// import os
///
/// cwd = os.getcwd()
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// cwd = Path.cwd()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd)
/// - [Python documentation: `os.getcwd`](https://docs.python.org/3/library/os.html#os.getcwd)
/// - [Python documentation: `os.getcwdb`](https://docs.python.org/3/library/os.html#os.getcwdb)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsGetcwd;
impl Violation for OsGetcwd {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.getcwd()` should be replaced by `Path.cwd()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.cwd()`".to_string())
}
}
/// PTH109
pub(crate) fn os_getcwd(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if !matches!(segments, ["os", "getcwd" | "getcwdb"]) {
return;
}
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsGetcwd, call.func.range());
if !call.arguments.is_empty() {
return;
}
if is_fix_os_getcwd_enabled(checker.settings()) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let replacement = format!("{binding}.cwd()");
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}
}

View File

@@ -7,8 +7,8 @@ use crate::checkers::ast::Checker;
use crate::rules::flake8_use_pathlib::helpers::is_keyword_only_argument_non_default;
use crate::rules::flake8_use_pathlib::rules::Glob;
use crate::rules::flake8_use_pathlib::violations::{
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathJoin,
OsPathSamefile, OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath,
BuiltinOpen, Joiner, OsChmod, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSamefile,
OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath,
};
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
@@ -83,10 +83,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
}
checker.report_diagnostic_if_enabled(OsReplace, range)
}
// PTH109
["os", "getcwd"] => checker.report_diagnostic_if_enabled(OsGetcwd, range),
["os", "getcwdb"] => checker.report_diagnostic_if_enabled(OsGetcwd, range),
// PTH116
["os", "stat"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.

View File

@@ -103,6 +103,7 @@ full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
17 | b = os.path.exists(p)
18 | bb = os.path.expanduser(p)
|
= help: Replace with `Path.cwd()`
full_name.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists()`
|
@@ -292,6 +293,7 @@ full_name.py:35:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
36 | os.path.join(p, *q)
37 | os.sep.join(p, *q)
|
= help: Replace with `Path.cwd()`
full_name.py:36:1: PTH118 `os.path.join()` should be replaced by `Path.joinpath()`
|
@@ -360,3 +362,21 @@ full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()`
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|
full_name.py:108:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
106 | os.replace("src", "dst", dst_dir_fd=2)
107 |
108 | os.getcwd()
| ^^^^^^^^^ PTH109
109 | os.getcwdb()
|
= help: Replace with `Path.cwd()`
full_name.py:109:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
108 | os.getcwd()
109 | os.getcwdb()
| ^^^^^^^^^^ PTH109
|
= help: Replace with `Path.cwd()`

View File

@@ -103,6 +103,7 @@ import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
17 | b = foo_p.exists(p)
18 | bb = foo_p.expanduser(p)
|
= help: Replace with `Path.cwd()`
import_as.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists()`
|

View File

@@ -103,6 +103,7 @@ import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
19 | b = exists(p)
20 | bb = expanduser(p)
|
= help: Replace with `Path.cwd()`
import_from.py:19:5: PTH110 `os.path.exists()` should be replaced by `Path.exists()`
|

View File

@@ -103,6 +103,7 @@ import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
24 | b = xexists(p)
25 | bb = xexpanduser(p)
|
= help: Replace with `Path.cwd()`
import_from_as.py:24:5: PTH110 `os.path.exists()` should be replaced by `Path.exists()`
|

View File

@@ -168,6 +168,7 @@ full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
17 | b = os.path.exists(p)
18 | bb = os.path.expanduser(p)
|
= help: Replace with `Path.cwd()`
full_name.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
@@ -510,6 +511,7 @@ full_name.py:35:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
36 | os.path.join(p, *q)
37 | os.sep.join(p, *q)
|
= help: Replace with `Path.cwd()`
full_name.py:36:1: PTH118 `os.path.join()` should be replaced by `Path.joinpath()`
|
@@ -578,3 +580,50 @@ full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()`
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|
full_name.py:108:1: PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
|
106 | os.replace("src", "dst", dst_dir_fd=2)
107 |
108 | os.getcwd()
| ^^^^^^^^^ PTH109
109 | os.getcwdb()
|
= help: Replace with `Path.cwd()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
105 106 | os.replace("src", "dst", src_dir_fd=1)
106 107 | os.replace("src", "dst", dst_dir_fd=2)
107 108 |
108 |-os.getcwd()
109 |+pathlib.Path.cwd()
109 110 | os.getcwdb()
full_name.py:109:1: PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
|
108 | os.getcwd()
109 | os.getcwdb()
| ^^^^^^^^^^ PTH109
|
= help: Replace with `Path.cwd()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
106 107 | os.replace("src", "dst", dst_dir_fd=2)
107 108 |
108 109 | os.getcwd()
109 |-os.getcwdb()
110 |+pathlib.Path.cwd()

View File

@@ -168,6 +168,7 @@ import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
17 | b = foo_p.exists(p)
18 | bb = foo_p.expanduser(p)
|
= help: Replace with `Path.cwd()`
import_as.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|

View File

@@ -172,6 +172,7 @@ import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
19 | b = exists(p)
20 | bb = expanduser(p)
|
= help: Replace with `Path.cwd()`
import_from.py:19:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|

View File

@@ -172,6 +172,7 @@ import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
24 | b = xexists(p)
25 | bb = xexpanduser(p)
|
= help: Replace with `Path.cwd()`
import_from_as.py:24:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|

View File

@@ -230,52 +230,6 @@ impl Violation for OsReplace {
}
}
/// ## What it does
/// Checks for uses of `os.getcwd` and `os.getcwdb`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.cwd()` can improve readability over the `os`
/// module's counterparts (e.g., `os.getcwd()`).
///
/// ## Examples
/// ```python
/// import os
///
/// cwd = os.getcwd()
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// cwd = Path.cwd()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd)
/// - [Python documentation: `os.getcwd`](https://docs.python.org/3/library/os.html#os.getcwd)
/// - [Python documentation: `os.getcwdb`](https://docs.python.org/3/library/os.html#os.getcwdb)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsGetcwd;
impl Violation for OsGetcwd {
#[derive_message_formats]
fn message(&self) -> String {
"`os.getcwd()` should be replaced by `Path.cwd()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.stat`.
///

View File

@@ -534,6 +534,7 @@ mod tests {
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_0.py"))]
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))]
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_2.py"))]
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_3.py"))]
#[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))]
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]

View File

@@ -1,8 +1,8 @@
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{
Arguments, CmpOp, Expr, ExprAttribute, ExprCall, ExprCompare, ExprContext, ExprStringLiteral,
ExprUnaryOp, Identifier, UnaryOp,
Arguments, CmpOp, Expr, ExprAttribute, ExprBytesLiteral, ExprCall, ExprCompare, ExprContext,
ExprStringLiteral, ExprUnaryOp, Identifier, UnaryOp,
};
use ruff_python_semantic::analyze::typing::find_binding_value;
use ruff_python_semantic::{Modules, SemanticModel};
@@ -72,6 +72,9 @@ impl Violation for UnnecessaryRegularExpression {
}
}
const METACHARACTERS: [char; 12] = ['.', '^', '$', '*', '+', '?', '{', '[', '\\', '|', '(', ')'];
const ESCAPABLE_SINGLE_CHARACTERS: &str = "abfnrtv";
/// RUF055
pub(crate) fn unnecessary_regular_expression(checker: &Checker, call: &ExprCall) {
// adapted from unraw_re_pattern
@@ -96,16 +99,19 @@ pub(crate) fn unnecessary_regular_expression(checker: &Checker, call: &ExprCall)
};
// For now, restrict this rule to string literals and variables that can be resolved to literals
let Some(string_lit) = resolve_string_literal(re_func.pattern, semantic) else {
let Some(literal) = resolve_literal(re_func.pattern, semantic) else {
return;
};
// For now, reject any regex metacharacters. Compare to the complete list
// from https://docs.python.org/3/howto/regex.html#matching-characters
let has_metacharacters = string_lit
.value
.to_str()
.contains(['.', '^', '$', '*', '+', '?', '{', '[', '\\', '|', '(', ')']);
let has_metacharacters = match &literal {
Literal::Str(str_lit) => str_lit.value.to_str().contains(METACHARACTERS),
Literal::Bytes(bytes_lit) => bytes_lit
.value
.iter()
.any(|part| part.iter().any(|&b| METACHARACTERS.contains(&(b as char)))),
};
if has_metacharacters {
return;
@@ -186,28 +192,48 @@ impl<'a> ReFunc<'a> {
// version
("sub", 3) => {
let repl = call.arguments.find_argument_value("repl", 1)?;
let lit = resolve_string_literal(repl, semantic)?;
let lit = resolve_literal(repl, semantic)?;
let mut fixable = true;
for (c, next) in lit.value.chars().tuple_windows() {
// `\0` (or any other ASCII digit) and `\g` have special meaning in `repl` strings.
// Meanwhile, nearly all other escapes of ASCII letters in a `repl` string causes
// `re.PatternError` to be raised at runtime.
//
// If we see that the escaped character is an alphanumeric ASCII character,
// we should only emit a diagnostic suggesting to replace the `re.sub()` call with
// `str.replace`if we can detect that the escaped character is one that is both
// valid in a `repl` string *and* does not have any special meaning in a REPL string.
//
// It's out of scope for this rule to change invalid `re.sub()` calls into something
// that would not raise an exception at runtime. They should be left as-is.
if c == '\\' && next.is_ascii_alphanumeric() {
if "abfnrtv".contains(next) {
fixable = false;
} else {
return None;
match lit {
Literal::Str(lit_str) => {
// Perform escape analysis for replacement literals.
for (c, next) in lit_str.value.to_str().chars().tuple_windows() {
// `\\0` (or any other ASCII digit) and `\\g` have special meaning in `repl` strings.
// Meanwhile, nearly all other escapes of ASCII letters in a `repl` string causes
// `re.PatternError` to be raised at runtime.
//
// If we see that the escaped character is an alphanumeric ASCII character,
// we should only emit a diagnostic suggesting to replace the `re.sub()` call with
// `str.replace`if we can detect that the escaped character is one that is both
// valid in a `repl` string *and* does not have any special meaning in a REPL string.
//
// It's out of scope for this rule to change invalid `re.sub()` calls into something
// that would not raise an exception at runtime. They should be left as-is.
if c == '\\' && next.is_ascii_alphanumeric() {
if ESCAPABLE_SINGLE_CHARACTERS.contains(next) {
fixable = false;
} else {
return None;
}
}
}
}
Literal::Bytes(lit_bytes) => {
for part in &lit_bytes.value {
for (byte, next) in part.iter().copied().tuple_windows() {
if byte == b'\\' && (next as char).is_ascii_alphanumeric() {
if ESCAPABLE_SINGLE_CHARACTERS.contains(next as char) {
fixable = false;
} else {
return None;
}
}
}
}
}
}
Some(ReFunc {
kind: ReFuncKind::Sub {
repl: fixable.then_some(repl),
@@ -329,6 +355,43 @@ impl<'a> ReFunc<'a> {
}
}
/// A literal that can be either a string or a bytes literal.
enum Literal<'a> {
Str(&'a ExprStringLiteral),
Bytes(&'a ExprBytesLiteral),
}
/// Try to resolve `name` to either a string or bytes literal in `semantic`.
fn resolve_literal<'a>(name: &'a Expr, semantic: &'a SemanticModel) -> Option<Literal<'a>> {
if let Some(str_lit) = resolve_string_literal(name, semantic) {
return Some(Literal::Str(str_lit));
}
if let Some(bytes_lit) = resolve_bytes_literal(name, semantic) {
return Some(Literal::Bytes(bytes_lit));
}
None
}
/// Try to resolve `name` to an [`ExprBytesLiteral`] in `semantic`.
fn resolve_bytes_literal<'a>(
name: &'a Expr,
semantic: &'a SemanticModel,
) -> Option<&'a ExprBytesLiteral> {
if name.is_bytes_literal_expr() {
return name.as_bytes_literal_expr();
}
if let Some(name_expr) = name.as_name_expr() {
let binding = semantic.binding(semantic.only_binding(name_expr)?);
let value = find_binding_value(binding, semantic)?;
if value.is_bytes_literal_expr() {
return value.as_bytes_literal_expr();
}
}
None
}
/// Try to resolve `name` to an [`ExprStringLiteral`] in `semantic`.
fn resolve_string_literal<'a>(
name: &'a Expr,

View File

@@ -0,0 +1,80 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF055_3.py:6:1: RUF055 [*] Plain string pattern passed to `re` function
|
5 | # Should be replaced with `b_src.replace(rb"x", b"y")`
6 | re.sub(rb"x", b"y", b_src)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
7 |
8 | # Should be replaced with `b_src.startswith(rb"abc")`
|
= help: Replace with `b_src.replace(rb"x", b"y")`
Safe fix
3 3 | b_src = b"abc"
4 4 |
5 5 | # Should be replaced with `b_src.replace(rb"x", b"y")`
6 |-re.sub(rb"x", b"y", b_src)
6 |+b_src.replace(rb"x", b"y")
7 7 |
8 8 | # Should be replaced with `b_src.startswith(rb"abc")`
9 9 | if re.match(rb"abc", b_src):
RUF055_3.py:9:4: RUF055 [*] Plain string pattern passed to `re` function
|
8 | # Should be replaced with `b_src.startswith(rb"abc")`
9 | if re.match(rb"abc", b_src):
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
10 | pass
|
= help: Replace with `b_src.startswith(rb"abc")`
Safe fix
6 6 | re.sub(rb"x", b"y", b_src)
7 7 |
8 8 | # Should be replaced with `b_src.startswith(rb"abc")`
9 |-if re.match(rb"abc", b_src):
9 |+if b_src.startswith(rb"abc"):
10 10 | pass
11 11 |
12 12 | # Should be replaced with `rb"x" in b_src`
RUF055_3.py:13:4: RUF055 [*] Plain string pattern passed to `re` function
|
12 | # Should be replaced with `rb"x" in b_src`
13 | if re.search(rb"x", b_src):
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF055
14 | pass
|
= help: Replace with `rb"x" in b_src`
Safe fix
10 10 | pass
11 11 |
12 12 | # Should be replaced with `rb"x" in b_src`
13 |-if re.search(rb"x", b_src):
13 |+if rb"x" in b_src:
14 14 | pass
15 15 |
16 16 | # Should be replaced with `b_src.split(rb"abc")`
RUF055_3.py:17:1: RUF055 [*] Plain string pattern passed to `re` function
|
16 | # Should be replaced with `b_src.split(rb"abc")`
17 | re.split(rb"abc", b_src)
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
18 |
19 | # Patterns containing metacharacters should NOT be replaced
|
= help: Replace with `b_src.split(rb"abc")`
Safe fix
14 14 | pass
15 15 |
16 16 | # Should be replaced with `b_src.split(rb"abc")`
17 |-re.split(rb"abc", b_src)
17 |+b_src.split(rb"abc")
18 18 |
19 19 | # Patterns containing metacharacters should NOT be replaced
20 20 | re.sub(rb"ab[c]", b"", b_src)

View File

@@ -1527,7 +1527,7 @@ impl<'src> Parser<'src> {
self.bump(kind.start_token());
let elements = self.parse_interpolated_string_elements(
flags,
InterpolatedStringElementsKind::Regular,
InterpolatedStringElementsKind::Regular(kind),
kind,
);

View File

@@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::error::UnsupportedSyntaxError;
use crate::parser::expression::ExpressionContext;
use crate::parser::progress::{ParserProgress, TokenId};
use crate::string::InterpolatedStringKind;
use crate::token::TokenValue;
use crate::token_set::TokenSet;
use crate::token_source::{TokenSource, TokenSourceCheckpoint};
@@ -799,7 +800,7 @@ impl WithItemKind {
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum InterpolatedStringElementsKind {
/// The regular f-string elements.
///
@@ -807,7 +808,7 @@ enum InterpolatedStringElementsKind {
/// ```py
/// f"hello {x:.2f} world"
/// ```
Regular,
Regular(InterpolatedStringKind),
/// The f-string elements are part of the format specifier.
///
@@ -819,15 +820,13 @@ enum InterpolatedStringElementsKind {
}
impl InterpolatedStringElementsKind {
const fn list_terminators(self) -> TokenSet {
const fn list_terminator(self) -> TokenKind {
match self {
InterpolatedStringElementsKind::Regular => {
TokenSet::new([TokenKind::FStringEnd, TokenKind::TStringEnd])
}
InterpolatedStringElementsKind::Regular(string_kind) => string_kind.end_token(),
// test_ok fstring_format_spec_terminator
// f"hello {x:} world"
// f"hello {x:.3f} world"
InterpolatedStringElementsKind::FormatSpec => TokenSet::new([TokenKind::Rbrace]),
InterpolatedStringElementsKind::FormatSpec => TokenKind::Rbrace,
}
}
}
@@ -1121,7 +1120,7 @@ impl RecoveryContextKind {
.then_some(ListTerminatorKind::Regular),
},
RecoveryContextKind::InterpolatedStringElements(kind) => {
if p.at_ts(kind.list_terminators()) {
if p.at(kind.list_terminator()) {
Some(ListTerminatorKind::Regular)
} else {
// test_err unterminated_fstring_newline_recovery
@@ -1177,13 +1176,23 @@ impl RecoveryContextKind {
) || p.at_name_or_soft_keyword()
}
RecoveryContextKind::WithItems(_) => p.at_expr(),
RecoveryContextKind::InterpolatedStringElements(_) => matches!(
p.current_token_kind(),
// Literal element
TokenKind::FStringMiddle | TokenKind::TStringMiddle
// Expression element
| TokenKind::Lbrace
),
RecoveryContextKind::InterpolatedStringElements(elements_kind) => {
match elements_kind {
InterpolatedStringElementsKind::Regular(interpolated_string_kind) => {
p.current_token_kind() == interpolated_string_kind.middle_token()
|| p.current_token_kind() == TokenKind::Lbrace
}
InterpolatedStringElementsKind::FormatSpec => {
matches!(
p.current_token_kind(),
// Literal element
TokenKind::FStringMiddle | TokenKind::TStringMiddle
// Expression element
| TokenKind::Lbrace
)
}
}
}
}
}
@@ -1272,8 +1281,8 @@ impl RecoveryContextKind {
),
},
RecoveryContextKind::InterpolatedStringElements(kind) => match kind {
InterpolatedStringElementsKind::Regular => ParseErrorType::OtherError(
"Expected an f-string or t-string element or the end of the f-string or t-string".to_string(),
InterpolatedStringElementsKind::Regular(string_kind) => ParseErrorType::OtherError(
format!("Expected an element of or the end of the {string_kind}"),
),
InterpolatedStringElementsKind::FormatSpec => ParseErrorType::OtherError(
"Expected an f-string or t-string element or a '}'".to_string(),
@@ -1316,8 +1325,9 @@ bitflags! {
const WITH_ITEMS_PARENTHESIZED = 1 << 25;
const WITH_ITEMS_PARENTHESIZED_EXPRESSION = 1 << 26;
const WITH_ITEMS_UNPARENTHESIZED = 1 << 28;
const FT_STRING_ELEMENTS = 1 << 29;
const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30;
const F_STRING_ELEMENTS = 1 << 29;
const T_STRING_ELEMENTS = 1 << 30;
const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 31;
}
}
@@ -1371,7 +1381,13 @@ impl RecoveryContext {
WithItemKind::Unparenthesized => RecoveryContext::WITH_ITEMS_UNPARENTHESIZED,
},
RecoveryContextKind::InterpolatedStringElements(kind) => match kind {
InterpolatedStringElementsKind::Regular => RecoveryContext::FT_STRING_ELEMENTS,
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::FString) => {
RecoveryContext::F_STRING_ELEMENTS
}
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString) => {
RecoveryContext::T_STRING_ELEMENTS
}
InterpolatedStringElementsKind::FormatSpec => {
RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC
}
@@ -1442,8 +1458,11 @@ impl RecoveryContext {
RecoveryContext::WITH_ITEMS_UNPARENTHESIZED => {
RecoveryContextKind::WithItems(WithItemKind::Unparenthesized)
}
RecoveryContext::FT_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
InterpolatedStringElementsKind::Regular,
RecoveryContext::F_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::FString),
),
RecoveryContext::T_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString),
),
RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC => {
RecoveryContextKind::InterpolatedStringElements(

View File

@@ -0,0 +1,10 @@
---
source: crates/ruff_python_parser/src/parser/tests.rs
expression: error
---
ParseError {
error: Lexical(
LineContinuationError,
),
location: 3..4,
}

View File

@@ -0,0 +1,12 @@
---
source: crates/ruff_python_parser/src/parser/tests.rs
expression: error
---
ParseError {
error: Lexical(
TStringError(
SingleRbrace,
),
),
location: 8..9,
}

View File

@@ -134,3 +134,26 @@ foo.bar[0].baz[2].egg??
.unwrap();
insta::assert_debug_snapshot!(parsed.syntax());
}
#[test]
fn test_fstring_expr_inner_line_continuation_and_t_string() {
let source = r#"f'{\t"i}'"#;
let parsed = parse_expression(source);
let error = parsed.unwrap_err();
insta::assert_debug_snapshot!(error);
}
#[test]
fn test_fstring_expr_inner_line_continuation_newline_t_string() {
let source = r#"f'{\
t"i}'"#;
let parsed = parse_expression(source);
let error = parsed.unwrap_err();
insta::assert_debug_snapshot!(error);
}

View File

@@ -41,7 +41,7 @@ impl From<StringType> for Expr {
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InterpolatedStringKind {
FString,
TString,

View File

@@ -124,5 +124,5 @@ Module(
|
1 | f"{lambda x: x}"
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string
| ^ Syntax Error: Expected an element of or the end of the f-string
|

View File

@@ -221,7 +221,7 @@ Module(
2 | 'hello'
3 | f'world {x}
4 | )
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string
| ^ Syntax Error: Expected an element of or the end of the f-string
5 | 1 + 1
6 | (
|

View File

@@ -128,5 +128,5 @@ Module(
|
1 | # parse_options: {"target-version": "3.14"}
2 | t"{lambda x: x}"
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string
| ^ Syntax Error: Expected an element of or the end of the t-string
|

View File

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

View File

@@ -77,10 +77,31 @@ struct ExpandedEdit {
content: Option<String>,
}
/// Perform global constructor initialization.
#[cfg(target_family = "wasm")]
#[expect(unsafe_code)]
pub fn before_main() {
unsafe extern "C" {
fn __wasm_call_ctors();
}
// Salsa uses the `inventory` crate, which registers global constructors that may need to be
// called explicitly on WASM. See <https://github.com/dtolnay/inventory/blob/master/src/lib.rs#L105>
// for details.
unsafe {
__wasm_call_ctors();
}
}
#[cfg(not(target_family = "wasm"))]
pub fn before_main() {}
#[wasm_bindgen(start)]
pub fn run() {
use log::Level;
before_main();
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.

View File

@@ -21,6 +21,8 @@ macro_rules! check {
#[wasm_bindgen_test]
fn empty_config() {
ruff_wasm::before_main();
check!(
"if (1, 2):\n pass",
r#"{}"#,
@@ -42,6 +44,8 @@ fn empty_config() {
#[wasm_bindgen_test]
fn syntax_error() {
ruff_wasm::before_main();
check!(
"x =\ny = 1\n",
r#"{}"#,
@@ -63,6 +67,8 @@ fn syntax_error() {
#[wasm_bindgen_test]
fn unsupported_syntax_error() {
ruff_wasm::before_main();
check!(
"match 2:\n case 1: ...",
r#"{"target-version": "py39"}"#,
@@ -84,11 +90,15 @@ fn unsupported_syntax_error() {
#[wasm_bindgen_test]
fn partial_config() {
ruff_wasm::before_main();
check!("if (1, 2):\n pass", r#"{"ignore": ["F"]}"#, []);
}
#[wasm_bindgen_test]
fn partial_nested_config() {
ruff_wasm::before_main();
let config = r#"{
"select": ["Q"],
"flake8-quotes": {

141
crates/ty/docs/rules.md generated
View File

@@ -36,7 +36,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L99)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L100)
</small>
**What it does**
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L143)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144)
</small>
**What it does**
@@ -88,7 +88,7 @@ f(int) # error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L170)
</small>
**What it does**
@@ -117,7 +117,7 @@ a = 1
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L194)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L195)
</small>
**What it does**
@@ -147,7 +147,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L220)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221)
</small>
**What it does**
@@ -177,7 +177,7 @@ class B(A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L264)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286)
</small>
**What it does**
@@ -202,7 +202,7 @@ class B(A, A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307)
</small>
**What it does**
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L427)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449)
</small>
**What it does**
@@ -334,7 +334,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L451)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L473)
</small>
**What it does**
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L317)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L339)
</small>
**What it does**
@@ -445,7 +445,7 @@ an atypical memory layout.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493)
</small>
**What it does**
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L511)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533)
</small>
**What it does**
@@ -496,7 +496,7 @@ a: int = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1515)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537)
</small>
**What it does**
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L555)
</small>
**What it does**
@@ -550,7 +550,7 @@ class A(42): ... # error: [invalid-base]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L584)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L606)
</small>
**What it does**
@@ -575,7 +575,7 @@ with 1:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L605)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627)
</small>
**What it does**
@@ -602,7 +602,7 @@ a: str
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650)
</small>
**What it does**
@@ -644,7 +644,7 @@ except ZeroDivisionError:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L664)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L686)
</small>
**What it does**
@@ -675,7 +675,7 @@ class C[U](Generic[T]): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L690)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712)
</small>
**What it does**
@@ -708,7 +708,7 @@ def f(t: TypeVar("U")): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L761)
</small>
**What it does**
@@ -740,7 +740,7 @@ class B(metaclass=f): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788)
</small>
**What it does**
@@ -788,7 +788,7 @@ def foo(x: int) -> int: ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L831)
</small>
**What it does**
@@ -812,7 +812,7 @@ def f(a: int = ''): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L399)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L421)
</small>
**What it does**
@@ -844,7 +844,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L829)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L851)
</small>
Checks for `raise` statements that raise non-exceptions or use invalid
@@ -891,7 +891,7 @@ def g():
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L492)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L514)
</small>
**What it does**
@@ -914,7 +914,7 @@ def func() -> int:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L872)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894)
</small>
**What it does**
@@ -968,7 +968,7 @@ TODO #14889
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L718)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L740)
</small>
**What it does**
@@ -993,7 +993,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L933)
</small>
**What it does**
@@ -1021,7 +1021,7 @@ TYPE_CHECKING = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L935)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957)
</small>
**What it does**
@@ -1049,7 +1049,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L987)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1009)
</small>
**What it does**
@@ -1081,7 +1081,7 @@ f(10) # Error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981)
</small>
**What it does**
@@ -1113,7 +1113,7 @@ class C:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1015)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1037)
</small>
**What it does**
@@ -1146,7 +1146,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1044)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1066)
</small>
**What it does**
@@ -1169,7 +1169,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085)
</small>
**What it does**
@@ -1196,7 +1196,7 @@ func("string") # error: [no-matching-overload]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1108)
</small>
**What it does**
@@ -1218,7 +1218,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1104)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1126)
</small>
**What it does**
@@ -1242,7 +1242,7 @@ for i in 34: # TypeError: 'int' object is not iterable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1155)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1177)
</small>
**What it does**
@@ -1296,7 +1296,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1491)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1513)
</small>
**What it does**
@@ -1324,7 +1324,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268)
</small>
**What it does**
@@ -1351,7 +1351,7 @@ class B(A): ... # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1313)
</small>
**What it does**
@@ -1376,7 +1376,7 @@ f("foo") # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1269)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291)
</small>
**What it does**
@@ -1402,7 +1402,7 @@ def _(x: int):
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1334)
</small>
**What it does**
@@ -1446,7 +1446,7 @@ class A:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1369)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1391)
</small>
**What it does**
@@ -1471,7 +1471,7 @@ f(x=1, y=2) # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1390)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412)
</small>
**What it does**
@@ -1497,7 +1497,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1434)
</small>
**What it does**
@@ -1520,7 +1520,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1431)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453)
</small>
**What it does**
@@ -1543,7 +1543,7 @@ print(x) # NameError: name 'x' is not defined
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1124)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1146)
</small>
**What it does**
@@ -1578,7 +1578,7 @@ b1 < b2 < b1 # exception raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1450)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472)
</small>
**What it does**
@@ -1604,7 +1604,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1494)
</small>
**What it does**
@@ -1622,6 +1622,31 @@ l = list(range(10))
l[1:10:0] # ValueError: slice step cannot be zero
```
## `deprecated`
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L265)
</small>
**What it does**
Checks for uses of deprecated items
**Why is this bad?**
Deprecated items should no longer be used.
**Examples**
```python
@warnings.deprecated("use new_func instead")
def old_func(): ...
old_func() # emits [deprecated] diagnostic
```
## `invalid-ignore-comment`
<small>
@@ -1655,7 +1680,7 @@ a = 20 / 0 # type: ignore
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198)
</small>
**What it does**
@@ -1681,7 +1706,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L117)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L118)
</small>
**What it does**
@@ -1711,7 +1736,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1220)
</small>
**What it does**
@@ -1741,7 +1766,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1543)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1565)
</small>
**What it does**
@@ -1766,7 +1791,7 @@ cast(int, f()) # Redundant
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1373)
</small>
**What it does**
@@ -1817,7 +1842,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1586)
</small>
**What it does**
@@ -1871,7 +1896,7 @@ def g():
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L551)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L573)
</small>
**What it does**
@@ -1908,7 +1933,7 @@ class D(C): ... # error: [unsupported-base]
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L246)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L247)
</small>
**What it does**
@@ -1930,7 +1955,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1224)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
</small>
**What it does**

View File

@@ -156,7 +156,12 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
Ok("short") => write!(stdout, "{}", db.salsa_memory_dump().display_short())?,
Ok("mypy_primer") => write!(stdout, "{}", db.salsa_memory_dump().display_mypy_primer())?,
Ok("full") => write!(stdout, "{}", db.salsa_memory_dump().display_full())?,
_ => {}
Ok(other) => {
tracing::warn!(
"Unknown value for `TY_MEMORY_REPORT`: `{other}`. Valid values are `short`, `mypy_primer`, and `full`."
);
}
Err(_) => {}
}
std::mem::forget(db);

View File

@@ -660,7 +660,7 @@ fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
--> test.py:4:13
|
2 | from typing_extensions import reveal_type
3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1...
3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +…
4 | reveal_type(total)
| ^^^^^ `Literal[2000]`
|

View File

@@ -15,7 +15,7 @@ use ty_project::metadata::pyproject::{PyProject, Tool};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
use ty_python_semantic::{ModuleName, PythonPlatform, resolve_module};
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module};
struct TestCase {
db: ProjectDatabase,
@@ -40,6 +40,14 @@ impl TestCase {
&self.db
}
/// Stops file-watching and returns the collected change events.
///
/// The caller must pass a `MatchEvent` filter that is applied to
/// the change events returned. To get all change events, use `|_:
/// &ChangeEvent| true`. If possible, callers should pass a filter for a
/// specific file name, e.g., `event_for_file("foo.py")`. When done this
/// way, the watcher will specifically try to wait for a change event
/// matching the filter. This can help avoid flakes.
#[track_caller]
fn stop_watch<M>(&mut self, matcher: M) -> Vec<ChangeEvent>
where
@@ -1877,3 +1885,156 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
Ok(())
}
/// This tests that retrieving submodules from a module has its cache
/// appropriately invalidated after a file is created.
#[test]
fn submodule_cache_invalidation_created() -> anyhow::Result<()> {
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
let get_submodules = |db: &dyn Db, module: &Module| {
let mut names = module
.all_submodules(db)
.iter()
.map(|name| name.as_str().to_string())
.collect::<Vec<String>>();
names.sort();
names.join("\n")
};
insta::assert_snapshot!(
get_submodules(case.db(), &module),
@"foo",
);
std::fs::write(case.project_path("bar/wazoo.py").as_std_path(), "")?;
let changes = case.stop_watch(event_for_file("wazoo.py"));
case.apply_changes(changes, None);
insta::assert_snapshot!(
get_submodules(case.db(), &module),
@r"
foo
wazoo
",
);
Ok(())
}
/// This tests that retrieving submodules from a module has its cache
/// appropriately invalidated after a file is deleted.
#[test]
fn submodule_cache_invalidation_deleted() -> anyhow::Result<()> {
let mut case = setup([
("lib.py", ""),
("bar/__init__.py", ""),
("bar/foo.py", ""),
("bar/wazoo.py", ""),
])?;
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
let get_submodules = |db: &dyn Db, module: &Module| {
let mut names = module
.all_submodules(db)
.iter()
.map(|name| name.as_str().to_string())
.collect::<Vec<String>>();
names.sort();
names.join("\n")
};
insta::assert_snapshot!(
get_submodules(case.db(), &module),
@r"
foo
wazoo
",
);
std::fs::remove_file(case.project_path("bar/wazoo.py").as_std_path())?;
let changes = case.stop_watch(event_for_file("wazoo.py"));
case.apply_changes(changes, None);
insta::assert_snapshot!(
get_submodules(case.db(), &module),
@"foo",
);
Ok(())
}
/// This tests that retrieving submodules from a module has its cache
/// appropriately invalidated after a file is created and then deleted.
#[test]
fn submodule_cache_invalidation_created_then_deleted() -> anyhow::Result<()> {
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
let get_submodules = |db: &dyn Db, module: &Module| {
let mut names = module
.all_submodules(db)
.iter()
.map(|name| name.as_str().to_string())
.collect::<Vec<String>>();
names.sort();
names.join("\n")
};
insta::assert_snapshot!(
get_submodules(case.db(), &module),
@"foo",
);
std::fs::write(case.project_path("bar/wazoo.py").as_std_path(), "")?;
let changes = case.take_watch_changes(event_for_file("wazoo.py"));
case.apply_changes(changes, None);
std::fs::remove_file(case.project_path("bar/wazoo.py").as_std_path())?;
let changes = case.stop_watch(event_for_file("wazoo.py"));
case.apply_changes(changes, None);
insta::assert_snapshot!(
get_submodules(case.db(), &module),
@"foo",
);
Ok(())
}
/// This tests that retrieving submodules from a module has its cache
/// appropriately invalidated after a file is created *after* a project
/// configuration change.
#[test]
fn submodule_cache_invalidation_after_pyproject_created() -> anyhow::Result<()> {
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
let get_submodules = |db: &dyn Db, module: &Module| {
let mut names = module
.all_submodules(db)
.iter()
.map(|name| name.as_str().to_string())
.collect::<Vec<String>>();
names.sort();
names.join("\n")
};
insta::assert_snapshot!(
get_submodules(case.db(), &module),
@"foo",
);
case.update_options(Options::default())?;
std::fs::write(case.project_path("bar/wazoo.py").as_std_path(), "")?;
let changes = case.take_watch_changes(event_for_file("wazoo.py"));
case.apply_changes(changes, None);
insta::assert_snapshot!(
get_submodules(case.db(), &module),
@r"
foo
wazoo
",
);
Ok(())
}

View File

@@ -1618,6 +1618,8 @@ Answer.<CURSOR>
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
__weakrefoffset__ :: int
_add_alias_ :: def _add_alias_(self, name: str) -> None
_add_value_alias_ :: def _add_value_alias_(self, value: Any) -> None
_generate_next_value_ :: def _generate_next_value_(name: str, start: int, count: int, last_values: list[Any]) -> Any
_ignore_ :: str | list[str]
_member_map_ :: dict[str, Enum]

View File

@@ -96,7 +96,7 @@ pub(crate) mod tests {
#[salsa::db]
impl SemanticDb for TestDb {
fn is_file_open(&self, file: File) -> bool {
fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path()
}

View File

@@ -112,6 +112,12 @@ impl<'a> CoveringNode<'a> {
Ok(self)
}
/// Returns an iterator over the ancestor nodes, starting from the root
/// and ending with the covering node.
pub(crate) fn ancestors(&self) -> impl Iterator<Item = AnyNodeRef<'a>> + '_ {
self.nodes.iter().copied()
}
/// Finds the index of the node that fully covers the range and
/// fulfills the given predicate.
///

View File

@@ -9,22 +9,53 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::types::Type;
use ty_python_semantic::{HasType, SemanticModel};
use ty_python_semantic::types::definitions_for_keyword_argument;
use ty_python_semantic::{
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
};
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub(crate) enum GotoTarget<'a> {
Expression(ast::ExprRef<'a>),
FunctionDef(&'a ast::StmtFunctionDef),
ClassDef(&'a ast::StmtClassDef),
Parameter(&'a ast::Parameter),
Alias(&'a ast::Alias),
/// Go to on the module name of an import from
/// Multi-part module names
/// Handles both `import foo.bar` and `from foo.bar import baz` cases
/// ```py
/// from foo import bar
/// ^^^
/// import foo.bar
/// ^^^
/// from foo.bar import baz
/// ^^^
/// ```
ImportedModule(&'a ast::StmtImportFrom),
ImportModuleComponent {
module_name: String,
component_index: usize,
component_range: TextRange,
},
/// Import alias in standard import statement
/// ```py
/// import foo.bar as baz
/// ^^^
/// ```
ImportModuleAlias {
alias: &'a ast::Alias,
},
/// Import alias in from import statement
/// ```py
/// from foo import bar as baz
/// ^^^
/// from foo import bar as baz
/// ^^^
/// ```
ImportSymbolAlias {
alias: &'a ast::Alias,
range: TextRange,
import_from: &'a ast::StmtImportFrom,
},
/// Go to on the exception handler variable
/// ```py
@@ -39,7 +70,10 @@ pub(crate) enum GotoTarget<'a> {
/// test(a = 1)
/// ^
/// ```
KeywordArgument(&'a ast::Keyword),
KeywordArgument {
keyword: &'a ast::Keyword,
call_expression: &'a ast::ExprCall,
},
/// Go to on the rest parameter of a pattern match
///
@@ -108,25 +142,22 @@ pub(crate) enum GotoTarget<'a> {
}
impl GotoTarget<'_> {
pub(crate) fn inferred_type<'db>(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
pub(crate) fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
let ty = match self {
GotoTarget::Expression(expression) => expression.inferred_type(model),
GotoTarget::FunctionDef(function) => function.inferred_type(model),
GotoTarget::ClassDef(class) => class.inferred_type(model),
GotoTarget::Parameter(parameter) => parameter.inferred_type(model),
GotoTarget::Alias(alias) => alias.inferred_type(model),
GotoTarget::ImportSymbolAlias { alias, .. } => alias.inferred_type(model),
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
GotoTarget::KeywordArgument(argument) => {
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
// than using the inferred value.
argument.value.inferred_type(model)
}
GotoTarget::KeywordArgument { keyword, .. } => keyword.value.inferred_type(model),
// TODO: Support identifier targets
GotoTarget::PatternMatchRest(_)
| GotoTarget::PatternKeywordArgument(_)
| GotoTarget::PatternMatchStarName(_)
| GotoTarget::PatternMatchAsName(_)
| GotoTarget::ImportedModule(_)
| GotoTarget::ImportModuleComponent { .. }
| GotoTarget::TypeParamTypeVarName(_)
| GotoTarget::TypeParamParamSpecName(_)
| GotoTarget::TypeParamTypeVarTupleName(_)
@@ -141,7 +172,7 @@ impl GotoTarget<'_> {
/// If a stub mapper is provided, definitions from stub files will be mapped to
/// their corresponding source file implementations.
pub(crate) fn get_definition_targets(
self,
&self,
file: ruff_db::files::File,
db: &dyn crate::Db,
stub_mapper: Option<&StubMapper>,
@@ -150,15 +181,19 @@ impl GotoTarget<'_> {
use ruff_python_ast as ast;
match self {
// For names, find the definitions of the symbol
GotoTarget::Expression(expression) => {
if let ast::ExprRef::Name(name) = expression {
Self::get_name_definition_targets(name, file, db, stub_mapper)
} else {
// For other expressions, we can't find definitions
None
}
}
GotoTarget::Expression(expression) => match expression {
ast::ExprRef::Name(name) => definitions_to_navigation_targets(
db,
stub_mapper,
definitions_for_name(db, file, name),
),
ast::ExprRef::Attribute(attribute) => definitions_to_navigation_targets(
db,
stub_mapper,
ty_python_semantic::definitions_for_attribute(db, file, attribute),
),
_ => None,
},
// For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => {
@@ -188,48 +223,57 @@ impl GotoTarget<'_> {
}))
}
// For imports, find the symbol being imported
GotoTarget::Alias(_alias) => {
// For aliases, we don't have the ExprName node, so we can't get the scope
// For now, return None. In the future, we could look up the imported symbol
None
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
GotoTarget::ImportSymbolAlias {
alias, import_from, ..
} => {
// Handle both original names and alias names in `from x import y as z` statements
let symbol_name = alias.name.as_str();
let definitions =
definitions_for_imported_symbol(db, file, import_from, symbol_name);
definitions_to_navigation_targets(db, stub_mapper, definitions)
}
GotoTarget::ImportModuleComponent {
module_name,
component_index,
..
} => {
// Handle both `import foo.bar` and `from foo.bar import baz` where offset is within module component
let components: Vec<&str> = module_name.split('.').collect();
// Build the module name up to and including the component containing the offset
let target_module_name = components[..=*component_index].join(".");
// Try to resolve the module
resolve_module_to_navigation_target(db, &target_module_name)
}
// Handle import aliases (offset within 'z' in "import x.y as z")
GotoTarget::ImportModuleAlias { alias } => {
// For import aliases, navigate to the module being aliased
// This only applies to regular import statements like "import x.y as z"
let full_module_name = alias.name.as_str();
// Try to resolve the module
resolve_module_to_navigation_target(db, full_module_name)
}
// Handle keyword arguments in call expressions
GotoTarget::KeywordArgument {
keyword,
call_expression,
} => {
let definitions =
definitions_for_keyword_argument(db, file, keyword, call_expression);
definitions_to_navigation_targets(db, stub_mapper, definitions)
}
// TODO: Handle attribute and method accesses (y in `x.y` expressions)
// TODO: Handle keyword arguments in call expression
// TODO: Handle multi-part module names in import statements
// TODO: Handle imported symbol in y in `from x import y as z` statement
// TODO: Handle string literals that map to TypedDict fields
_ => None,
}
}
/// Get navigation targets for definitions associated with a name expression
fn get_name_definition_targets(
name: &ruff_python_ast::ExprName,
file: ruff_db::files::File,
db: &dyn crate::Db,
stub_mapper: Option<&StubMapper>,
) -> Option<crate::NavigationTargets> {
use ty_python_semantic::definitions_for_name;
// Get all definitions for this name
let mut definitions = definitions_for_name(db, file, name);
// Apply stub mapping if a mapper is provided
if let Some(mapper) = stub_mapper {
definitions = mapper.map_definitions(definitions);
}
if definitions.is_empty() {
return None;
}
// Convert definitions to navigation targets
let targets = convert_resolved_definitions_to_targets(db, definitions);
Some(crate::NavigationTargets::unique(targets))
}
}
impl Ranged for GotoTarget<'_> {
@@ -239,10 +283,13 @@ impl Ranged for GotoTarget<'_> {
GotoTarget::FunctionDef(function) => function.name.range,
GotoTarget::ClassDef(class) => class.name.range,
GotoTarget::Parameter(parameter) => parameter.name.range,
GotoTarget::Alias(alias) => alias.name.range,
GotoTarget::ImportedModule(module) => module.module.as_ref().unwrap().range,
GotoTarget::ImportSymbolAlias { range, .. } => *range,
GotoTarget::ImportModuleComponent {
component_range, ..
} => *component_range,
GotoTarget::ImportModuleAlias { alias } => alias.asname.as_ref().unwrap().range,
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
GotoTarget::KeywordArgument(keyword) => keyword.arg.as_ref().unwrap().range,
GotoTarget::KeywordArgument { keyword, .. } => keyword.arg.as_ref().unwrap().range,
GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range,
GotoTarget::PatternKeywordArgument(keyword) => keyword.attr.range,
GotoTarget::PatternMatchStarName(star) => star.name.as_ref().unwrap().range,
@@ -279,18 +326,35 @@ fn convert_resolved_definitions_to_targets(
full_range: full_range.range(),
}
}
ty_python_semantic::ResolvedDefinition::ModuleFile(module_file) => {
// For module files, navigate to the beginning of the file
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
// For file ranges, navigate to the specific range within the file
crate::NavigationTarget {
file: module_file,
focus_range: ruff_text_size::TextRange::default(), // Start of file
full_range: ruff_text_size::TextRange::default(), // Start of file
file: file_range.file(),
focus_range: file_range.range(),
full_range: file_range.range(),
}
}
})
.collect()
}
/// Shared helper to map and convert resolved definitions into navigation targets.
fn definitions_to_navigation_targets<'db>(
db: &dyn crate::Db,
stub_mapper: Option<&StubMapper<'db>>,
mut definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
) -> Option<crate::NavigationTargets> {
if let Some(mapper) = stub_mapper {
definitions = mapper.map_definitions(definitions);
}
if definitions.is_empty() {
None
} else {
let targets = convert_resolved_definitions_to_targets(db, definitions);
Some(crate::NavigationTargets::unique(targets))
}
}
pub(crate) fn find_goto_target(
parsed: &ParsedModuleRef,
offset: TextSize,
@@ -318,12 +382,102 @@ pub(crate) fn find_goto_target(
Some(AnyNodeRef::StmtFunctionDef(function)) => Some(GotoTarget::FunctionDef(function)),
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
Some(AnyNodeRef::Alias(alias)) => Some(GotoTarget::Alias(alias)),
Some(AnyNodeRef::StmtImportFrom(from)) => Some(GotoTarget::ImportedModule(from)),
Some(AnyNodeRef::Alias(alias)) => {
// Find the containing import statement to determine the type
let import_stmt = covering_node.ancestors().find(|node| {
matches!(
node,
AnyNodeRef::StmtImport(_) | AnyNodeRef::StmtImportFrom(_)
)
});
match import_stmt {
Some(AnyNodeRef::StmtImport(_)) => {
// Regular import statement like "import x.y as z"
// Is the offset within the alias name (asname) part?
if let Some(asname) = &alias.asname {
if asname.range.contains_inclusive(offset) {
return Some(GotoTarget::ImportModuleAlias { alias });
}
}
// Is the offset in the module name part?
if alias.name.range.contains_inclusive(offset) {
let full_name = alias.name.as_str();
if let Some((component_index, component_range)) =
find_module_component(full_name, alias.name.range.start(), offset)
{
return Some(GotoTarget::ImportModuleComponent {
module_name: full_name.to_string(),
component_index,
component_range,
});
}
}
None
}
Some(AnyNodeRef::StmtImportFrom(import_from)) => {
// From import statement like "from x import y as z"
// Is the offset within the alias name (asname) part?
if let Some(asname) = &alias.asname {
if asname.range.contains_inclusive(offset) {
return Some(GotoTarget::ImportSymbolAlias {
alias,
range: asname.range,
import_from,
});
}
}
// Is the offset in the original name part?
if alias.name.range.contains_inclusive(offset) {
return Some(GotoTarget::ImportSymbolAlias {
alias,
range: alias.name.range,
import_from,
});
}
None
}
_ => None,
}
}
Some(AnyNodeRef::StmtImportFrom(from)) => {
// Handle offset within module name in from import statements
if let Some(module_expr) = &from.module {
let full_module_name = module_expr.to_string();
if let Some((component_index, component_range)) =
find_module_component(&full_module_name, module_expr.range.start(), offset)
{
return Some(GotoTarget::ImportModuleComponent {
module_name: full_module_name,
component_index,
component_range,
});
}
}
None
}
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
Some(GotoTarget::ExceptVariable(handler))
}
Some(AnyNodeRef::Keyword(keyword)) => Some(GotoTarget::KeywordArgument(keyword)),
Some(AnyNodeRef::Keyword(keyword)) => {
// Find the containing call expression from the ancestor chain
let call_expression = covering_node
.ancestors()
.find_map(ruff_python_ast::AnyNodeRef::expr_call)?;
Some(GotoTarget::KeywordArgument {
keyword,
call_expression,
})
}
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
Some(GotoTarget::PatternMatchRest(mapping))
}
@@ -361,3 +515,57 @@ pub(crate) fn find_goto_target(
node => node.as_expr_ref().map(GotoTarget::Expression),
}
}
/// Helper function to resolve a module name and create a navigation target.
fn resolve_module_to_navigation_target(
db: &dyn crate::Db,
module_name_str: &str,
) -> Option<crate::NavigationTargets> {
use ty_python_semantic::{ModuleName, resolve_module};
if let Some(module_name) = ModuleName::new(module_name_str) {
if let Some(resolved_module) = resolve_module(db, &module_name) {
if let Some(module_file) = resolved_module.file() {
return Some(crate::NavigationTargets::single(crate::NavigationTarget {
file: module_file,
focus_range: TextRange::default(),
full_range: TextRange::default(),
}));
}
}
}
None
}
/// Helper function to extract module component information from a dotted module name
fn find_module_component(
full_module_name: &str,
module_start: TextSize,
offset: TextSize,
) -> Option<(usize, TextRange)> {
let pos_in_module = offset - module_start;
let pos_in_module = pos_in_module.to_usize();
// Split the module name into components and find which one contains the offset
let mut current_pos = 0;
let components: Vec<&str> = full_module_name.split('.').collect();
for (i, component) in components.iter().enumerate() {
let component_start = current_pos;
let component_end = current_pos + component.len();
// Check if the offset is within this component or at its right boundary
if pos_in_module >= component_start && pos_in_module <= component_end {
let component_range = TextRange::new(
module_start + TextSize::from(u32::try_from(component_start).ok()?),
module_start + TextSize::from(u32::try_from(component_end).ok()?),
);
return Some((i, component_range));
}
// Move past this component and the dot
current_pos = component_end + 1; // +1 for the dot
}
None
}

View File

@@ -611,7 +611,330 @@ def another_helper():
}
#[test]
fn goto_declaration_builtin_type() {
fn goto_declaration_import_as_alias_name() {
let test = CursorTest::builder()
.source(
"main.py",
"
import mymodule.submodule as su<CURSOR>b
print(sub.helper())
",
)
.source(
"mymodule/__init__.py",
"
# Main module init
",
)
.source(
"mymodule/submodule.py",
r#"
FOO = 0
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule/submodule.py:1:1
|
1 |
| ^
2 | FOO = 0
|
info: Source
--> main.py:2:30
|
2 | import mymodule.submodule as sub
| ^^^
3 | print(sub.helper())
|
");
}
#[test]
fn goto_declaration_import_as_alias_name_on_module() {
let test = CursorTest::builder()
.source(
"main.py",
"
import mymodule.submod<CURSOR>ule as sub
print(sub.helper())
",
)
.source(
"mymodule/__init__.py",
"
# Main module init
",
)
.source(
"mymodule/submodule.py",
r#"
FOO = 0
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule/submodule.py:1:1
|
1 |
| ^
2 | FOO = 0
|
info: Source
--> main.py:2:17
|
2 | import mymodule.submodule as sub
| ^^^^^^^^^
3 | print(sub.helper())
|
");
}
#[test]
fn goto_declaration_from_import_symbol_original() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mypackage.utils import hel<CURSOR>per as h
result = h("/a", "/b")
"#,
)
.source(
"mypackage/__init__.py",
r#"
# Package init
"#,
)
.source(
"mypackage/utils.py",
r#"
def helper(a, b):
return a + "/" + b
def another_helper(path):
return "processed"
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mypackage/utils.py:2:5
|
2 | def helper(a, b):
| ^^^^^^
3 | return a + "/" + b
|
info: Source
--> main.py:2:29
|
2 | from mypackage.utils import helper as h
| ^^^^^^
3 | result = h("/a", "/b")
|
"#);
}
#[test]
fn goto_declaration_from_import_symbol_alias() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mypackage.utils import helper as h<CURSOR>
result = h("/a", "/b")
"#,
)
.source(
"mypackage/__init__.py",
r#"
# Package init
"#,
)
.source(
"mypackage/utils.py",
r#"
def helper(a, b):
return a + "/" + b
def another_helper(path):
return "processed"
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mypackage/utils.py:2:5
|
2 | def helper(a, b):
| ^^^^^^
3 | return a + "/" + b
|
info: Source
--> main.py:2:39
|
2 | from mypackage.utils import helper as h
| ^
3 | result = h("/a", "/b")
|
"#);
}
#[test]
fn goto_declaration_from_import_module() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mypackage.ut<CURSOR>ils import helper as h
result = h("/a", "/b")
"#,
)
.source(
"mypackage/__init__.py",
r#"
# Package init
"#,
)
.source(
"mypackage/utils.py",
r#"
def helper(a, b):
return a + "/" + b
def another_helper(path):
return "processed"
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mypackage/utils.py:1:1
|
1 |
| ^
2 | def helper(a, b):
3 | return a + "/" + b
|
info: Source
--> main.py:2:16
|
2 | from mypackage.utils import helper as h
| ^^^^^
3 | result = h("/a", "/b")
|
"#);
}
#[test]
fn goto_declaration_instance_attribute() {
let test = cursor_test(
"
class C:
def __init__(self):
self.x: int = 1
c = C()
y = c.x<CURSOR>
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:4:21
|
2 | class C:
3 | def __init__(self):
4 | self.x: int = 1
| ^^^^^^
5 |
6 | c = C()
|
info: Source
--> main.py:7:17
|
6 | c = C()
7 | y = c.x
| ^^^
|
");
}
#[test]
fn goto_declaration_instance_attribute_no_annotation() {
let test = cursor_test(
"
class C:
def __init__(self):
self.x = 1
c = C()
y = c.x<CURSOR>
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:4:21
|
2 | class C:
3 | def __init__(self):
4 | self.x = 1
| ^^^^^^
5 |
6 | c = C()
|
info: Source
--> main.py:7:17
|
6 | c = C()
7 | y = c.x
| ^^^
|
");
}
#[test]
fn goto_declaration_method_call_to_definition() {
let test = cursor_test(
"
class C:
def foo(self):
return 42
c = C()
res = c.foo<CURSOR>()
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:3:21
|
2 | class C:
3 | def foo(self):
| ^^^
4 | return 42
|
info: Source
--> main.py:7:19
|
6 | c = C()
7 | res = c.foo()
| ^^^^^
|
");
}
#[test]
fn goto_declaration_module_attribute() {
let test = cursor_test(
r#"
x: i<CURSOR>nt = 42
@@ -721,6 +1044,152 @@ def function():
"#);
}
#[test]
fn goto_declaration_inherited_attribute() {
let test = cursor_test(
"
class A:
x = 10
class B(A):
pass
b = B()
y = b.x<CURSOR>
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:3:17
|
2 | class A:
3 | x = 10
| ^
4 |
5 | class B(A):
|
info: Source
--> main.py:9:17
|
8 | b = B()
9 | y = b.x
| ^^^
|
");
}
#[test]
fn goto_declaration_property_getter_setter() {
let test = cursor_test(
"
class C:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
c = C()
c.value<CURSOR> = 42
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:7:21
|
6 | @property
7 | def value(self):
| ^^^^^
8 | return self._value
|
info: Source
--> main.py:11:13
|
10 | c = C()
11 | c.value = 42
| ^^^^^^^
|
");
}
#[test]
fn goto_declaration_function_doc_attribute() {
let test = cursor_test(
r#"
def my_function():
"""This is a docstring."""
return 42
doc = my_function.__doc<CURSOR>__
"#,
);
// Should navigate to the __doc__ property in the FunctionType class in typeshed
let result = test.goto_declaration();
assert!(
!result.contains("No goto target found"),
"Should find builtin __doc__ attribute"
);
assert!(
!result.contains("No declarations found"),
"Should find builtin __doc__ declarations"
);
// Should navigate to a typeshed file containing the __doc__ attribute
assert!(
result.contains("types.pyi") || result.contains("builtins.pyi"),
"Should navigate to typeshed file with __doc__ definition"
);
assert!(
result.contains("__doc__"),
"Should find the __doc__ attribute definition"
);
assert!(
result.contains("info[goto-declaration]: Declaration"),
"Should be a goto-declaration result"
);
}
#[test]
fn goto_declaration_protocol_instance_attribute() {
let test = cursor_test(
"
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
name: str
def use_drawable(obj: Drawable):
obj.na<CURSOR>me
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:6:17
|
4 | class Drawable(Protocol):
5 | def draw(self) -> None: ...
6 | name: str
| ^^^^
7 |
8 | def use_drawable(obj: Drawable):
|
info: Source
--> main.py:9:17
|
8 | def use_drawable(obj: Drawable):
9 | obj.name
| ^^^^^^^^
|
");
}
#[test]
fn goto_declaration_generic_method_class_type() {
let test = cursor_test(
@@ -756,6 +1225,94 @@ class MyClass:
");
}
#[test]
fn goto_declaration_keyword_argument_simple() {
let test = cursor_test(
"
def my_function(x, y, z=10):
return x + y + z
result = my_function(1, y<CURSOR>=2, z=3)
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:32
|
2 | def my_function(x, y, z=10):
| ^
3 | return x + y + z
|
info: Source
--> main.py:5:37
|
3 | return x + y + z
4 |
5 | result = my_function(1, y=2, z=3)
| ^
|
");
}
#[test]
fn goto_declaration_keyword_argument_overloaded() {
let test = cursor_test(
r#"
from typing import overload
@overload
def process(data: str, format: str) -> str: ...
@overload
def process(data: int, format: int) -> int: ...
def process(data, format):
return data
# Call the overloaded function
result = process("hello", format<CURSOR>="json")
"#,
);
// Should navigate to the parameter in both matching overloads
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:5:36
|
4 | @overload
5 | def process(data: str, format: str) -> str: ...
| ^^^^^^
6 |
7 | @overload
|
info: Source
--> main.py:14:39
|
13 | # Call the overloaded function
14 | result = process("hello", format="json")
| ^^^^^^
|
info[goto-declaration]: Declaration
--> main.py:8:36
|
7 | @overload
8 | def process(data: int, format: int) -> int: ...
| ^^^^^^
9 |
10 | def process(data, format):
|
info: Source
--> main.py:14:39
|
13 | # Call the overloaded function
14 | result = process("hello", format="json")
| ^^^^^^
|
"#);
}
impl CursorTest {
fn goto_declaration(&self) -> String {
let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset)

View File

@@ -198,14 +198,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:13
@@ -227,14 +227,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:2:22
@@ -343,14 +343,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:18
@@ -378,14 +378,14 @@ mod tests {
// is an int. Navigating to `str` would match pyright's behavior.
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:338:7
--> stdlib/builtins.pyi:337:7
|
336 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
337 |
338 | class int:
335 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
336 |
337 | class int:
| ^^^
339 | """int([x]) -> integer
340 | int(x, base=10) -> integer
338 | """int([x]) -> integer
339 | int(x, base=10) -> integer
|
info: Source
--> main.py:4:18
@@ -412,14 +412,14 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:2892:7
--> stdlib/builtins.pyi:2888:7
|
2890 | """See PEP 585"""
2891 |
2892 | class dict(MutableMapping[_KT, _VT]):
2886 | """See PEP 585"""
2887 |
2888 | class dict(MutableMapping[_KT, _VT]):
| ^^^^
2893 | """dict() -> new empty dictionary
2894 | dict(mapping) -> new dictionary initialized from a mapping object's
2889 | """dict() -> new empty dictionary
2890 | dict(mapping) -> new dictionary initialized from a mapping object's
|
info: Source
--> main.py:6:5
@@ -443,14 +443,14 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:17
@@ -536,14 +536,14 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:27
@@ -567,13 +567,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/types.pyi:922:11
--> stdlib/types.pyi:921:11
|
920 | if sys.version_info >= (3, 10):
921 | @final
922 | class NoneType:
919 | if sys.version_info >= (3, 10):
920 | @final
921 | class NoneType:
| ^^^^^^^^
923 | """The type of the None singleton."""
922 | """The type of the None singleton."""
|
info: Source
--> main.py:3:17
@@ -584,14 +584,14 @@ f(**kwargs<CURSOR>)
|
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:17

View File

@@ -105,11 +105,11 @@ pub struct NavigationTargets(smallvec::SmallVec<[NavigationTarget; 1]>);
impl NavigationTargets {
fn single(target: NavigationTarget) -> Self {
Self(smallvec::smallvec![target])
Self(smallvec::smallvec_inline![target])
}
fn empty() -> Self {
Self(smallvec::SmallVec::new())
Self(smallvec::SmallVec::new_const())
}
fn unique(targets: impl IntoIterator<Item = NavigationTarget>) -> Self {

View File

@@ -482,25 +482,40 @@ impl<'db> SemanticTokenVisitor<'db> {
parameters: &ast::Parameters,
func: Option<&ast::StmtFunctionDef>,
) {
// Parameters
for (i, param) in parameters.args.iter().enumerate() {
let token_type = if let Some(func) = func {
// For function definitions, use the classification logic to determine
// whether this is a self/cls parameter or just a regular parameter
self.classify_parameter(&param.parameter, i == 0, func)
} else {
// For lambdas, all parameters are just parameters (no self/cls)
SemanticTokenType::Parameter
let mut param_index = 0;
for any_param in parameters {
let parameter = any_param.as_parameter();
let token_type = match any_param {
ast::AnyParameterRef::NonVariadic(_) => {
// For non-variadic parameters (positional-only, regular, keyword-only),
// check if this should be classified as self/cls parameter
if let Some(func) = func {
let result = self.classify_parameter(parameter, param_index == 0, func);
param_index += 1;
result
} else {
// For lambdas, all parameters are just parameters (no self/cls)
param_index += 1;
SemanticTokenType::Parameter
}
}
ast::AnyParameterRef::Variadic(_) => {
// Variadic parameters (*args, **kwargs) are always just parameters
param_index += 1;
SemanticTokenType::Parameter
}
};
self.add_token(
param.parameter.name.range(),
parameter.name.range(),
token_type,
SemanticTokenModifier::empty(),
);
// Handle parameter type annotations
if let Some(annotation) = &param.parameter.annotation {
if let Some(annotation) = &parameter.annotation {
self.visit_type_annotation(annotation);
}
}
@@ -977,7 +992,8 @@ class MyClass:
class MyClass:
def method(instance, x): pass
@classmethod
def other(klass, y): pass<CURSOR>
def other(klass, y): pass
def complex_method(instance, posonly, /, regular, *args, kwonly, **kwargs): pass<CURSOR>
",
);
@@ -992,6 +1008,13 @@ class MyClass:
"other" @ 75..80: Method [definition]
"klass" @ 81..86: ClsParameter
"y" @ 88..89: Parameter
"complex_method" @ 105..119: Method [definition]
"instance" @ 120..128: SelfParameter
"posonly" @ 130..137: Parameter
"regular" @ 142..149: Parameter
"args" @ 152..156: Parameter
"kwonly" @ 158..164: Parameter
"kwargs" @ 168..174: Parameter
"#);
}
@@ -1665,6 +1688,12 @@ class BoundedContainer[T: int, U = str]:
"P" @ 324..325: Variable
"str" @ 327..330: Class
"wrapper" @ 341..348: Function [definition]
"args" @ 350..354: Parameter
"P" @ 356..357: Variable
"args" @ 358..362: Variable
"kwargs" @ 366..372: Parameter
"P" @ 374..375: Variable
"kwargs" @ 376..382: Variable
"str" @ 387..390: Class
"str" @ 407..410: Class
"func" @ 411..415: Variable

View File

@@ -12,8 +12,8 @@ use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files};
use ruff_db::system::System;
use ruff_db::vendored::VendoredFileSystem;
use salsa::Event;
use salsa::plumbing::ZalsaDatabase;
use salsa::{Event, Setter};
use ty_ide::Db as IdeDb;
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{Db as SemanticDb, Program};
@@ -82,22 +82,25 @@ impl ProjectDatabase {
Ok(db)
}
/// Checks all open files in the project and its dependencies.
/// Checks the files in the project and its dependencies as per the project's check mode.
///
/// Use [`set_check_mode`] to update the check mode.
///
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
pub fn check(&self) -> Vec<Diagnostic> {
self.check_with_mode(CheckMode::OpenFiles)
}
/// Checks all open files in the project and its dependencies, using the given reporter.
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, CheckMode::OpenFiles, reporter)
}
/// Check the project with the given mode.
pub fn check_with_mode(&self, mode: CheckMode) -> Vec<Diagnostic> {
let mut reporter = DummyReporter;
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter);
self.project().check(self, mode, reporter)
self.project().check(self, reporter)
}
/// Checks the files in the project and its dependencies, using the given reporter.
///
/// Use [`set_check_mode`] to update the check mode.
///
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, reporter)
}
#[tracing::instrument(level = "debug", skip(self))]
@@ -105,6 +108,12 @@ impl ProjectDatabase {
self.project().check_file(self, file)
}
/// Set the check mode for the project.
pub fn set_check_mode(&mut self, mode: CheckMode) {
tracing::debug!("Updating project to check {mode}");
self.project().set_check_mode(self).to(mode);
}
/// Returns a mutable reference to the system.
///
/// WARNING: Triggers a new revision, canceling other database handles. This can lead to deadlock.
@@ -163,17 +172,28 @@ impl std::fmt::Debug for ProjectDatabase {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize))]
pub enum CheckMode {
/// Checks only the open files in the project.
/// Checks the open files in the project.
OpenFiles,
/// Checks all files in the project, ignoring the open file set.
///
/// This includes virtual files, such as those created by the language server.
/// This includes virtual files, such as those opened in an editor.
#[default]
AllFiles,
}
impl fmt::Display for CheckMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CheckMode::OpenFiles => write!(f, "open files"),
CheckMode::AllFiles => write!(f, "all files"),
}
}
}
/// Stores memory usage information.
pub struct SalsaMemoryDump {
total_fields: usize,
@@ -389,12 +409,9 @@ impl IdeDb for ProjectDatabase {}
#[salsa::db]
impl SemanticDb for ProjectDatabase {
fn is_file_open(&self, file: File) -> bool {
let Some(project) = &self.project else {
return false;
};
project.is_file_open(self, file)
fn should_check_file(&self, file: File) -> bool {
self.project
.is_some_and(|project| project.should_check_file(self, file))
}
fn rule_selection(&self, file: File) -> &RuleSelection {
@@ -543,7 +560,7 @@ pub(crate) mod tests {
#[salsa::db]
impl ty_python_semantic::Db for TestDb {
fn is_file_open(&self, file: ruff_db::files::File) -> bool {
fn should_check_file(&self, file: ruff_db::files::File) -> bool {
!file.path(self).is_vendored_path()
}

View File

@@ -6,7 +6,8 @@ use std::collections::BTreeSet;
use crate::walk::ProjectFilesWalker;
use ruff_db::Db as _;
use ruff_db::files::{File, Files};
use ruff_db::file_revision::FileRevision;
use ruff_db::files::{File, FileRootKind, Files};
use ruff_db::system::SystemPath;
use rustc_hash::FxHashSet;
use salsa::Setter;
@@ -57,12 +58,6 @@ impl ProjectDatabase {
let mut synced_files = FxHashSet::default();
let mut sync_recursively = BTreeSet::default();
let mut sync_path = |db: &mut ProjectDatabase, path: &SystemPath| {
if synced_files.insert(path.to_path_buf()) {
File::sync_path(db, path);
}
};
for change in changes {
tracing::trace!("Handle change: {:?}", change);
@@ -92,12 +87,49 @@ impl ProjectDatabase {
match change {
ChangeEvent::Changed { path, kind: _ } | ChangeEvent::Opened(path) => {
sync_path(self, &path);
if synced_files.insert(path.to_path_buf()) {
let absolute =
SystemPath::absolute(&path, self.system().current_directory());
File::sync_path_only(self, &absolute);
if let Some(root) = self.files().root(self, &absolute) {
match root.kind_at_time_of_creation(self) {
// When a file inside the root of
// the project is changed, we don't
// want to mark the entire root as
// having changed too. In theory it
// might make sense to, but at time
// of writing, the file root revision
// on a project is used to invalidate
// the submodule files found within a
// directory. If we bumped the revision
// on every change within a project,
// then this caching technique would be
// effectively useless.
//
// It's plausible we should explore
// a more robust cache invalidation
// strategy that models more directly
// what we care about. For example, by
// keeping track of directories and
// their direct children explicitly,
// and then keying the submodule cache
// off of that instead. ---AG
FileRootKind::Project => {}
FileRootKind::LibrarySearchPath => {
root.set_revision(self).to(FileRevision::now());
}
}
}
}
}
ChangeEvent::Created { kind, path } => {
match kind {
CreatedKind::File => sync_path(self, &path),
CreatedKind::File => {
if synced_files.insert(path.to_path_buf()) {
File::sync_path(self, &path);
}
}
CreatedKind::Directory | CreatedKind::Any => {
sync_recursively.insert(path.clone());
}
@@ -138,7 +170,9 @@ impl ProjectDatabase {
};
if is_file {
sync_path(self, &path);
if synced_files.insert(path.to_path_buf()) {
File::sync_path(self, &path);
}
if let Some(file) = self.files().try_system(self, &path) {
project.remove_file(self, file);

View File

@@ -18,7 +18,7 @@ use crate::{IOErrorDiagnostic, Project};
/// The implementation uses internal mutability to transition between the lazy and indexed state
/// without triggering a new salsa revision. This is safe because the initial indexing happens on first access,
/// so no query can be depending on the contents of the indexed files before that. All subsequent mutations to
/// the indexed files must go through `IndexedMut`, which uses the Salsa setter `package.set_file_set` to
/// the indexed files must go through `IndexedMut`, which uses the Salsa setter `project.set_file_set` to
/// ensure that Salsa always knows when the set of indexed files have changed.
#[derive(Debug)]
pub struct IndexedFiles {
@@ -280,7 +280,7 @@ mod tests {
// Calling files a second time should not dead-lock.
// This can e.g. happen when `check_file` iterates over all files and
// `is_file_open` queries the open files.
// `should_check_file` queries the open files.
let files_2 = project.file_set(&db).get();
match files_2 {

View File

@@ -6,7 +6,7 @@ use files::{Index, Indexed, IndexedFiles};
use metadata::settings::Settings;
pub use metadata::{ProjectMetadata, ProjectMetadataError};
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic};
use ruff_db::files::File;
use ruff_db::files::{File, FileRootKind};
use ruff_db::parsed::parsed_module;
use ruff_db::source::{SourceTextError, source_text};
use ruff_db::system::{SystemPath, SystemPathBuf};
@@ -14,6 +14,8 @@ use rustc_hash::FxHashSet;
use salsa::Durability;
use salsa::Setter;
use std::backtrace::BacktraceStatus;
use std::collections::hash_set;
use std::iter::FusedIterator;
use std::panic::{AssertUnwindSafe, UnwindSafe};
use std::sync::Arc;
use thiserror::Error;
@@ -54,13 +56,10 @@ pub fn default_lints_registry() -> LintRegistry {
#[salsa::input]
#[derive(Debug)]
pub struct Project {
/// The files that are open in the project.
///
/// Setting the open files to a non-`None` value changes `check` to only check the
/// open files rather than all files in the project.
#[returns(as_deref)]
/// The files that are open in the project, [`None`] if there are no open files.
#[returns(ref)]
#[default]
open_fileset: Option<Arc<FxHashSet<File>>>,
open_fileset: FxHashSet<File>,
/// The first-party files of this project.
#[default]
@@ -110,6 +109,13 @@ pub struct Project {
/// Diagnostics that were generated when resolving the project settings.
#[returns(deref)]
settings_diagnostics: Vec<OptionDiagnostic>,
/// The mode in which the project should be checked.
///
/// This changes the behavior of `check` to either check only the open files or all files in
/// the project including the virtual files that might exists in the editor.
#[default]
check_mode: CheckMode,
}
/// A progress reporter.
@@ -135,6 +141,13 @@ impl Project {
pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Result<Self, ToSettingsError> {
let (settings, diagnostics) = metadata.options().to_settings(db, metadata.root())?;
// This adds a file root for the project itself. This enables
// tracking of when changes are made to the files in a project
// at the directory level. At time of writing (2025-07-17),
// this is used for caching completions for submodules.
db.files()
.try_add_root(db, metadata.root(), FileRootKind::Project);
let project = Project::builder(Box::new(metadata), Box::new(settings), diagnostics)
.durability(Durability::MEDIUM)
.open_fileset_durability(Durability::LOW)
@@ -207,17 +220,20 @@ impl Project {
self.reload_files(db);
}
/// Checks all open files in the project and its dependencies.
/// Checks the project and its dependencies according to the project's check mode.
pub(crate) fn check(
self,
db: &ProjectDatabase,
mode: CheckMode,
mut reporter: AssertUnwindSafe<&mut dyn ProgressReporter>,
) -> Vec<Diagnostic> {
let project_span = tracing::debug_span!("Project::check");
let _span = project_span.enter();
tracing::debug!("Checking project '{name}'", name = self.name(db));
tracing::debug!(
"Checking {} in project '{name}'",
self.check_mode(db),
name = self.name(db)
);
let mut diagnostics: Vec<Diagnostic> = Vec::new();
diagnostics.extend(
@@ -226,11 +242,7 @@ impl Project {
.map(OptionDiagnostic::to_diagnostic),
);
let files = match mode {
CheckMode::OpenFiles => ProjectFiles::new(db, self),
// TODO: Consider open virtual files as well
CheckMode::AllFiles => ProjectFiles::Indexed(self.files(db)),
};
let files = ProjectFiles::new(db, self);
reporter.set_files(files.len());
diagnostics.extend(
@@ -240,6 +252,7 @@ impl Project {
.map(IOErrorDiagnostic::to_diagnostic),
);
let open_files = self.open_files(db);
let check_start = ruff_db::Instant::now();
let file_diagnostics = std::sync::Mutex::new(vec![]);
@@ -257,11 +270,30 @@ impl Project {
tracing::debug_span!(parent: project_span, "check_file", ?file);
let _entered = check_file_span.entered();
let result = check_file_impl(&db, file);
file_diagnostics
.lock()
.unwrap()
.extend(result.iter().map(Clone::clone));
match check_file_impl(&db, file) {
Ok(diagnostics) => {
file_diagnostics
.lock()
.unwrap()
.extend(diagnostics.iter().map(Clone::clone));
// This is outside `check_file_impl` to avoid that opening or closing
// a file invalidates the `check_file_impl` query of every file!
if !open_files.contains(&file) {
// The module has already been parsed by `check_file_impl`.
// We only retrieve it here so that we can call `clear` on it.
let parsed = parsed_module(&db, file);
// Drop the AST now that we are done checking this file. It is not currently open,
// so it is unlikely to be accessed again soon. If any queries need to access the AST
// from across files, it will be re-parsed.
parsed.clear();
}
}
Err(io_error) => {
file_diagnostics.lock().unwrap().push(io_error.clone());
}
}
reporter.report_file(&file);
});
@@ -284,16 +316,17 @@ impl Project {
}
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
if !self.is_file_open(db, file) {
if !self.should_check_file(db, file) {
return Vec::new();
}
check_file_impl(db, file).iter().map(Clone::clone).collect()
match check_file_impl(db, file) {
Ok(diagnostics) => diagnostics.to_vec(),
Err(diagnostic) => vec![diagnostic.clone()],
}
}
/// Opens a file in the project.
///
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
pub fn open_file(self, db: &mut dyn Db, file: File) {
tracing::debug!("Opening file `{}`", file.path(db));
@@ -340,45 +373,40 @@ impl Project {
}
}
/// Returns the open files in the project or `None` if the entire project should be checked.
pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet<File>> {
/// Returns the open files in the project or `None` if there are no open files.
pub fn open_files(self, db: &dyn Db) -> &FxHashSet<File> {
self.open_fileset(db)
}
/// Sets the open files in the project.
///
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
#[tracing::instrument(level = "debug", skip(self, db))]
pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet<File>) {
tracing::debug!("Set open project files (count: {})", open_files.len());
self.set_open_fileset(db).to(Some(Arc::new(open_files)));
self.set_open_fileset(db).to(open_files);
}
/// This takes the open files from the project and returns them.
///
/// This changes the behavior of `check` to check all files in the project instead of just the open files.
fn take_open_files(self, db: &mut dyn Db) -> FxHashSet<File> {
tracing::debug!("Take open project files");
// Salsa will cancel any pending queries and remove its own reference to `open_files`
// so that the reference counter to `open_files` now drops to 1.
let open_files = self.set_open_fileset(db).to(None);
if let Some(open_files) = open_files {
Arc::try_unwrap(open_files).unwrap()
} else {
FxHashSet::default()
}
self.set_open_fileset(db).to(FxHashSet::default())
}
/// Returns `true` if the file is open in the project.
/// Returns `true` if the file should be checked.
///
/// A file is considered open when:
/// * explicitly set as an open file using [`open_file`](Self::open_file)
/// * It has a [`SystemPath`] and belongs to a package's `src` files
/// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath)
pub fn is_file_open(self, db: &dyn Db, file: File) -> bool {
/// This depends on the project's check mode:
/// * For [`OpenFiles`], it checks if the file is either explicitly set as an open file using
/// [`open_file`] or a system virtual path
/// * For [`AllFiles`], it checks if the file is either a system virtual path or a part of the
/// indexed files in the project
///
/// [`open_file`]: Self::open_file
/// [`OpenFiles`]: CheckMode::OpenFiles
/// [`AllFiles`]: CheckMode::AllFiles
pub fn should_check_file(self, db: &dyn Db, file: File) -> bool {
let path = file.path(db);
// Try to return early to avoid adding a dependency on `open_files` or `file_set` which
@@ -387,12 +415,12 @@ impl Project {
return false;
}
if let Some(open_files) = self.open_files(db) {
open_files.contains(&file)
} else if file.path(db).is_system_path() {
self.files(db).contains(&file)
} else {
file.path(db).is_system_virtual_path()
match self.check_mode(db) {
CheckMode::OpenFiles => self.open_files(db).contains(&file),
CheckMode::AllFiles => {
// Virtual files are always checked.
path.is_system_virtual_path() || self.files(db).contains(&file)
}
}
}
@@ -469,24 +497,29 @@ impl Project {
self.set_file_set(db).to(IndexedFiles::lazy());
}
}
/// Check if the project's settings have any issues
pub fn check_settings(&self, db: &dyn Db) -> Vec<Diagnostic> {
self.settings_diagnostics(db)
.iter()
.map(OptionDiagnostic::to_diagnostic)
.collect()
}
}
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)]
pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Result<Box<[Diagnostic]>, Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
// Abort checking if there are IO errors.
let source = source_text(db, file);
if let Some(read_error) = source.read_error() {
diagnostics.push(
IOErrorDiagnostic {
file: Some(file),
error: read_error.clone().into(),
}
.to_diagnostic(),
);
return diagnostics.into_boxed_slice();
return Err(IOErrorDiagnostic {
file: Some(file),
error: read_error.clone().into(),
}
.to_diagnostic());
}
let parsed = parsed_module(db, file);
@@ -516,17 +549,6 @@ pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
}
}
if db
.project()
.open_fileset(db)
.is_none_or(|files| !files.contains(&file))
{
// Drop the AST now that we are done checking this file. It is not currently open,
// so it is unlikely to be accessed again soon. If any queries need to access the AST
// from across files, it will be re-parsed.
parsed.clear();
}
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
.primary_span()
@@ -535,7 +557,7 @@ pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
.start()
});
diagnostics.into_boxed_slice()
Ok(diagnostics.into_boxed_slice())
}
#[derive(Debug)]
@@ -546,24 +568,23 @@ enum ProjectFiles<'a> {
impl<'a> ProjectFiles<'a> {
fn new(db: &'a dyn Db, project: Project) -> Self {
if let Some(open_files) = project.open_files(db) {
ProjectFiles::OpenFiles(open_files)
} else {
ProjectFiles::Indexed(project.files(db))
match project.check_mode(db) {
CheckMode::OpenFiles => ProjectFiles::OpenFiles(project.open_files(db)),
CheckMode::AllFiles => ProjectFiles::Indexed(project.files(db)),
}
}
fn diagnostics(&self) -> &[IOErrorDiagnostic] {
match self {
ProjectFiles::OpenFiles(_) => &[],
ProjectFiles::Indexed(indexed) => indexed.diagnostics(),
ProjectFiles::Indexed(files) => files.diagnostics(),
}
}
fn len(&self) -> usize {
match self {
ProjectFiles::OpenFiles(open_files) => open_files.len(),
ProjectFiles::Indexed(indexed) => indexed.len(),
ProjectFiles::Indexed(files) => files.len(),
}
}
}
@@ -575,16 +596,14 @@ impl<'a> IntoIterator for &'a ProjectFiles<'a> {
fn into_iter(self) -> Self::IntoIter {
match self {
ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()),
ProjectFiles::Indexed(indexed) => ProjectFilesIter::Indexed {
files: indexed.into_iter(),
},
ProjectFiles::Indexed(files) => ProjectFilesIter::Indexed(files.into_iter()),
}
}
}
enum ProjectFilesIter<'db> {
OpenFiles(std::collections::hash_set::Iter<'db, File>),
Indexed { files: files::IndexedIter<'db> },
OpenFiles(hash_set::Iter<'db, File>),
Indexed(files::IndexedIter<'db>),
}
impl Iterator for ProjectFilesIter<'_> {
@@ -593,11 +612,13 @@ impl Iterator for ProjectFilesIter<'_> {
fn next(&mut self) -> Option<Self::Item> {
match self {
ProjectFilesIter::OpenFiles(files) => files.next().copied(),
ProjectFilesIter::Indexed { files } => files.next(),
ProjectFilesIter::Indexed(files) => files.next(),
}
}
}
impl FusedIterator for ProjectFilesIter<'_> {}
#[derive(Debug, Clone)]
pub struct IOErrorDiagnostic {
file: Option<File>,
@@ -754,10 +775,11 @@ mod tests {
assert_eq!(source_text(&db, file).as_str(), "");
assert_eq!(
check_file_impl(&db, file)
.iter()
.map(|diagnostic| diagnostic.primary_message().to_string())
.collect::<Vec<_>>(),
vec!["Failed to read file: No such file or directory".to_string()]
.as_ref()
.unwrap_err()
.primary_message()
.to_string(),
"Failed to read file: No such file or directory".to_string()
);
let events = db.take_salsa_events();
@@ -770,6 +792,8 @@ mod tests {
assert_eq!(source_text(&db, file).as_str(), "");
assert_eq!(
check_file_impl(&db, file)
.as_ref()
.unwrap()
.iter()
.map(|diagnostic| diagnostic.primary_message().to_string())
.collect::<Vec<_>>(),

View File

@@ -372,11 +372,11 @@ mod tests {
with_escaped_paths(|| {
assert_ron_snapshot!(&project, @r#"
ProjectMetadata(
name: Name("app"),
root: "/app",
options: Options(),
)
ProjectMetadata(
name: Name("app"),
root: "/app",
options: Options(),
)
"#);
});
@@ -410,11 +410,11 @@ mod tests {
with_escaped_paths(|| {
assert_ron_snapshot!(&project, @r#"
ProjectMetadata(
name: Name("backend"),
root: "/app",
options: Options(),
)
ProjectMetadata(
name: Name("backend"),
root: "/app",
options: Options(),
)
"#);
});
@@ -552,16 +552,16 @@ unclosed table, expected `]`
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
Ok(())

View File

@@ -1806,7 +1806,7 @@ class Frozen:
raise AttributeError("Attributes can not be modified")
instance = Frozen()
instance.non_existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `non_existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
instance.non_existing = 2 # error: [invalid-assignment] "Can not assign to unresolved attribute `non_existing` on type `Frozen`"
instance.existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
```

View File

@@ -415,8 +415,7 @@ frozen_instance = MyFrozenGeneric[int](1)
frozen_instance.x = 2 # error: [invalid-assignment]
```
When attempting to mutate an unresolved attribute on a frozen dataclass, only `unresolved-attribute`
is emitted:
Attempting to mutate an unresolved attribute on a frozen dataclass:
```py
from dataclasses import dataclass
@@ -425,7 +424,39 @@ from dataclasses import dataclass
class MyFrozenClass: ...
frozen = MyFrozenClass()
frozen.x = 2 # error: [unresolved-attribute]
frozen.x = 2 # error: [invalid-assignment] "Can not assign to unresolved attribute `x` on type `MyFrozenClass`"
```
A diagnostic is also emitted if a frozen dataclass is inherited, and an attempt is made to mutate an
attribute in the child class:
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1
class MyFrozenChildClass(MyFrozenClass): ...
frozen = MyFrozenChildClass()
frozen.x = 2 # error: [invalid-assignment]
```
The same diagnostic is emitted if a frozen dataclass is inherited, and an attempt is made to delete
an attribute:
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1
class MyFrozenChildClass(MyFrozenClass): ...
frozen = MyFrozenChildClass()
del frozen.x # TODO this should emit an [invalid-assignment]
```
### `match_args`
@@ -467,7 +498,7 @@ class C:
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
c = C(1)
# TODO: this should be an error
# error: [invalid-assignment] "Cannot assign to final attribute `instance_variable` on type `C`"
c.instance_variable = 2
```

View File

@@ -40,22 +40,92 @@ else:
# error: [possibly-unresolved-reference]
reveal_type(c) # revealed: Literal[2]
d = 1
d = [1, 2, 3]
def delete():
# TODO: this results in `UnboundLocalError`; we should emit `unresolved-reference`
del d
del d # error: [unresolved-reference] "Name `d` used when not defined"
delete()
reveal_type(d) # revealed: Literal[1]
reveal_type(d) # revealed: list[Unknown]
def delete_element():
# When the `del` target isn't a name, it doesn't force local resolution.
del d[0]
print(d)
def delete_global():
global d
del d
# We could lint that `d` is unbound in this trivial case, but because it's global we'd need to
# be careful about false positives if `d` got reinitialized somehow in between the two `del`s.
del d
delete_global()
# The variable should have been removed, but we won't check it for now.
reveal_type(d) # revealed: Literal[1]
# Again, the variable should have been removed, but we don't check it.
reveal_type(d) # revealed: list[Unknown]
def delete_nonlocal():
e = 2
def delete_nonlocal_bad():
del e # error: [unresolved-reference] "Name `e` used when not defined"
def delete_nonlocal_ok():
nonlocal e
del e
# As with `global` above, we don't track that the nonlocal `e` is unbound.
del e
```
## `del` forces local resolution even if it's unreachable
Without a `global x` or `nonlocal x` declaration in `foo`, `del x` in `foo` causes `print(x)` in an
inner function `bar` to resolve to `foo`'s binding, in this case an unresolved reference / unbound
local error:
```py
x = 1
def foo():
print(x) # error: [unresolved-reference] "Name `x` used when not defined"
if False:
# Assigning to `x` would have the same effect here.
del x
def bar():
print(x) # error: [unresolved-reference] "Name `x` used when not defined"
```
## But `del` doesn't force local resolution of `global` or `nonlocal` variables
However, with `global x` in `foo`, `print(x)` in `bar` resolves in the global scope, despite the
`del` in `foo`:
```py
x = 1
def foo():
global x
def bar():
# allowed, refers to `x` in the global scope
reveal_type(x) # revealed: Unknown | Literal[1]
bar()
del x # allowed, deletes `x` in the global scope (though we don't track that)
```
`nonlocal x` has a similar effect, if we add an extra `enclosing` scope to give it something to
refer to:
```py
def enclosing():
x = 2
def foo():
nonlocal x
def bar():
# allowed, refers to `x` in `enclosing`
reveal_type(x) # revealed: Unknown | Literal[2]
bar()
del x # allowed, deletes `x` in `enclosing` (though we don't track that)
```
## Delete attributes

View File

@@ -0,0 +1,355 @@
# Tests for the `@deprecated` decorator
## Introduction
<!-- snapshot-diagnostics -->
The decorator `@deprecated("some message")` can be applied to functions, methods, overloads, and
classes. Uses of these items should subsequently produce a warning.
```py
from typing_extensions import deprecated
@deprecated("use OtherClass")
def myfunc(): ...
myfunc() # error: [deprecated] "use OtherClass"
```
```py
from typing_extensions import deprecated
@deprecated("use BetterClass")
class MyClass: ...
MyClass() # error: [deprecated] "use BetterClass"
```
```py
from typing_extensions import deprecated
class MyClass:
@deprecated("use something else")
def afunc(): ...
@deprecated("don't use this!")
def amethod(self): ...
MyClass.afunc() # error: [deprecated] "use something else"
MyClass().amethod() # error: [deprecated] "don't use this!"
```
## Syntax
<!-- snapshot-diagnostics -->
The typeshed declaration of the decorator is as follows:
```ignore
class deprecated:
message: LiteralString
category: type[Warning] | None
stacklevel: int
def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
def __call__(self, arg: _T, /) -> _T: ...
```
Only the mandatory message string is of interest to static analysis, the other two affect only
runtime behaviour.
```py
from typing_extensions import deprecated
@deprecated # error: [invalid-argument-type] "LiteralString"
def invalid_deco(): ...
invalid_deco() # error: [missing-argument]
```
```py
from typing_extensions import deprecated
@deprecated() # error: [missing-argument] "message"
def invalid_deco(): ...
invalid_deco()
```
The argument is supposed to be a LiteralString, and we can handle simple constant propagations like
this:
```py
from typing_extensions import deprecated
x = "message"
@deprecated(x)
def invalid_deco(): ...
invalid_deco() # error: [deprecated] "message"
```
However sufficiently opaque LiteralStrings we can't resolve, and so we lose the message:
```py
from typing_extensions import deprecated, LiteralString
def opaque() -> LiteralString:
return "message"
@deprecated(opaque())
def valid_deco(): ...
valid_deco() # error: [deprecated]
```
Fully dynamic strings are technically allowed at runtime, but typeshed mandates that the input is a
LiteralString, so we can/should emit a diagnostic for this:
```py
from typing_extensions import deprecated
def opaque() -> str:
return "message"
@deprecated(opaque()) # error: [invalid-argument-type] "LiteralString"
def dubious_deco(): ...
dubious_deco()
```
Although we have no use for the other arguments, we should still error if they're wrong.
```py
from typing_extensions import deprecated
@deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf"
def invalid_deco(): ...
invalid_deco()
```
And we should always handle correct ones fine.
```py
from typing_extensions import deprecated
@deprecated("some message", category=DeprecationWarning, stacklevel=1)
def valid_deco(): ...
valid_deco() # error: [deprecated] "some message"
```
## Different Versions
There are 2 different sources of `@deprecated`: `warnings` and `typing_extensions`. The version in
`warnings` was added in 3.13, the version in `typing_extensions` is a compatibility shim.
```toml
[environment]
python-version = "3.13"
```
`main.py`:
```py
import warnings
import typing_extensions
@warnings.deprecated("nope")
def func1(): ...
@typing_extensions.deprecated("nada")
def func2(): ...
func1() # error: [deprecated] "nope"
func2() # error: [deprecated] "nada"
```
## Imports
### Direct Import Deprecated
Importing a deprecated item should produce a warning. Subsequent uses of the deprecated item
shouldn't produce a warning.
`module.py`:
```py
from typing_extensions import deprecated
@deprecated("Use OtherType instead")
class DeprType: ...
@deprecated("Use other_func instead")
def depr_func(): ...
```
`main.py`:
```py
# error: [deprecated] "Use OtherType instead"
# error: [deprecated] "Use other_func instead"
from module import DeprType, depr_func
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
DeprType() # error: [deprecated] "Use OtherType instead"
depr_func() # error: [deprecated] "Use other_func instead"
def higher_order(x): ...
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
higher_order(DeprType) # error: [deprecated] "Use OtherType instead"
higher_order(depr_func) # error: [deprecated] "Use other_func instead"
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
DeprType.__str__ # error: [deprecated] "Use OtherType instead"
depr_func.__str__ # error: [deprecated] "Use other_func instead"
```
### Non-Import Deprecated
If the items aren't imported and instead referenced using `module.item` then each use should produce
a warning.
`module.py`:
```py
from typing_extensions import deprecated
@deprecated("Use OtherType instead")
class DeprType: ...
@deprecated("Use other_func instead")
def depr_func(): ...
```
`main.py`:
```py
import module
module.DeprType() # error: [deprecated] "Use OtherType instead"
module.depr_func() # error: [deprecated] "Use other_func instead"
def higher_order(x): ...
higher_order(module.DeprType) # error: [deprecated] "Use OtherType instead"
higher_order(module.depr_func) # error: [deprecated] "Use other_func instead"
module.DeprType.__str__ # error: [deprecated] "Use OtherType instead"
module.depr_func.__str__ # error: [deprecated] "Use other_func instead"
```
### Star Import Deprecated
If the items are instead star-imported, then the actual uses should warn.
`module.py`:
```py
from typing_extensions import deprecated
@deprecated("Use OtherType instead")
class DeprType: ...
@deprecated("Use other_func instead")
def depr_func(): ...
```
`main.py`:
```py
from module import *
DeprType() # error: [deprecated] "Use OtherType instead"
depr_func() # error: [deprecated] "Use other_func instead"
def higher_order(x): ...
higher_order(DeprType) # error: [deprecated] "Use OtherType instead"
higher_order(depr_func) # error: [deprecated] "Use other_func instead"
DeprType.__str__ # error: [deprecated] "Use OtherType instead"
depr_func.__str__ # error: [deprecated] "Use other_func instead"
```
## Aliases
Ideally a deprecated warning shouldn't transitively follow assignments, as you already had to "name"
the deprecated symbol to assign it to something else. These kinds of diagnostics would therefore be
redundant and annoying.
```py
from typing_extensions import deprecated
@deprecated("Use OtherType instead")
class DeprType: ...
@deprecated("Use other_func instead")
def depr_func(): ...
alias_func = depr_func # error: [deprecated] "Use other_func instead"
AliasClass = DeprType # error: [deprecated] "Use OtherType instead"
# TODO: these diagnostics ideally shouldn't fire
alias_func() # error: [deprecated] "Use other_func instead"
AliasClass() # error: [deprecated] "Use OtherType instead"
```
## Dunders
If a dunder like `__add__` is deprecated, then the equivalent syntactic sugar like `+` should fire a
diagnostic.
```py
from typing_extensions import deprecated
class MyInt:
def __init__(self, val):
self.val = val
@deprecated("MyInt `+` support is broken")
def __add__(self, other):
return MyInt(self.val + other.val)
x = MyInt(1)
y = MyInt(2)
z = x + y # TODO error: [deprecated] "MyInt `+` support is broken"
```
## Overloads
Overloads can be deprecated, but only trigger warnings when invoked.
```py
from typing_extensions import deprecated
from typing_extensions import overload
@overload
@deprecated("strings are no longer supported")
def f(x: str): ...
@overload
def f(x: int): ...
def f(x):
print(x)
f(1)
f("hello") # TODO: error: [deprecated] "strings are no longer supported"
```
If the actual impl is deprecated, the deprecation always fires.
```py
from typing_extensions import deprecated
from typing_extensions import overload
@overload
def f(x: str): ...
@overload
def f(x: int): ...
@deprecated("unusable")
def f(x):
print(x)
f(1) # error: [deprecated] "unusable"
f("hello") # error: [deprecated] "unusable"
```

View File

@@ -72,7 +72,14 @@ reveal_type(my_bool(0)) # revealed: bool
## Truthy values
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Literal
reveal_type(bool(1)) # revealed: Literal[True]
reveal_type(bool((0,))) # revealed: Literal[True]
reveal_type(bool("NON EMPTY")) # revealed: Literal[True]
@@ -81,6 +88,42 @@ reveal_type(bool(True)) # revealed: Literal[True]
def foo(): ...
reveal_type(bool(foo)) # revealed: Literal[True]
class SingleElementTupleSubclass(tuple[int]): ...
reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True]
reveal_type(SingleElementTupleSubclass.__bool__) # revealed: (self: tuple[int], /) -> Literal[True]
reveal_type(SingleElementTupleSubclass().__bool__) # revealed: () -> Literal[True]
# Unknown length, but we know the length is guaranteed to be >=2
class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ...
reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True]
reveal_type(MixedTupleSubclass.__bool__) # revealed: (self: tuple[int, *tuple[str, ...], bytes], /) -> Literal[True]
reveal_type(MixedTupleSubclass().__bool__) # revealed: () -> Literal[True]
# Unknown length with an overridden `__bool__`:
class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]):
def __bool__(self) -> Literal[True]:
return True
reveal_type(bool(VariadicTupleSubclassWithDunderBoolOverride((1,)))) # revealed: Literal[True]
reveal_type(VariadicTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True]
# revealed: bound method VariadicTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
reveal_type(VariadicTupleSubclassWithDunderBoolOverride().__bool__)
# Same again but for a subclass of a fixed-length tuple:
class EmptyTupleSubclassWithDunderBoolOverride(tuple[()]):
# TODO: we should reject this override as a Liskov violation:
def __bool__(self) -> Literal[True]:
return True
reveal_type(bool(EmptyTupleSubclassWithDunderBoolOverride(()))) # revealed: Literal[True]
reveal_type(EmptyTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True]
# revealed: bound method EmptyTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
reveal_type(EmptyTupleSubclassWithDunderBoolOverride().__bool__)
```
## Falsy values
@@ -92,6 +135,12 @@ reveal_type(bool(None)) # revealed: Literal[False]
reveal_type(bool("")) # revealed: Literal[False]
reveal_type(bool(False)) # revealed: Literal[False]
reveal_type(bool()) # revealed: Literal[False]
class EmptyTupleSubclass(tuple[()]): ...
reveal_type(bool(EmptyTupleSubclass())) # revealed: Literal[False]
reveal_type(EmptyTupleSubclass.__bool__) # revealed: (self: tuple[()], /) -> Literal[False]
reveal_type(EmptyTupleSubclass().__bool__) # revealed: () -> Literal[False]
```
## Ambiguous values
@@ -100,6 +149,13 @@ reveal_type(bool()) # revealed: Literal[False]
reveal_type(bool([])) # revealed: bool
reveal_type(bool({})) # revealed: bool
reveal_type(bool(set())) # revealed: bool
class VariadicTupleSubclass(tuple[int, ...]): ...
def f(x: tuple[int, ...], y: VariadicTupleSubclass):
reveal_type(bool(x)) # revealed: bool
reveal_type(x.__bool__) # revealed: () -> bool
reveal_type(y.__bool__) # revealed: () -> bool
```
## `__bool__` returning `NoReturn`

View File

@@ -65,6 +65,51 @@ reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
reveal_type(len((*[], *{}))) # revealed: Literal[2]
```
Tuple subclasses:
```py
class EmptyTupleSubclass(tuple[()]): ...
class Length1TupleSubclass(tuple[int]): ...
class Length2TupleSubclass(tuple[int, str]): ...
class UnknownLengthTupleSubclass(tuple[int, ...]): ...
reveal_type(len(EmptyTupleSubclass())) # revealed: Literal[0]
reveal_type(len(Length1TupleSubclass((1,)))) # revealed: Literal[1]
reveal_type(len(Length2TupleSubclass((1, "foo")))) # revealed: Literal[2]
reveal_type(len(UnknownLengthTupleSubclass((1, 2, 3)))) # revealed: int
reveal_type(tuple[int, int].__len__) # revealed: (self: tuple[int, int], /) -> Literal[2]
reveal_type(tuple[int, ...].__len__) # revealed: (self: tuple[int, ...], /) -> int
def f(x: tuple[int, int], y: tuple[int, ...]):
reveal_type(x.__len__) # revealed: () -> Literal[2]
reveal_type(y.__len__) # revealed: () -> int
reveal_type(EmptyTupleSubclass.__len__) # revealed: (self: tuple[()], /) -> Literal[0]
reveal_type(EmptyTupleSubclass().__len__) # revealed: () -> Literal[0]
reveal_type(UnknownLengthTupleSubclass.__len__) # revealed: (self: tuple[int, ...], /) -> int
reveal_type(UnknownLengthTupleSubclass().__len__) # revealed: () -> int
```
If `__len__` is overridden, we use the overridden return type:
```py
from typing import Literal
class UnknownLengthSubclassWithDunderLenOverridden(tuple[int, ...]):
def __len__(self) -> Literal[42]:
return 42
reveal_type(len(UnknownLengthSubclassWithDunderLenOverridden())) # revealed: Literal[42]
class FixedLengthSubclassWithDunderLenOverridden(tuple[int]):
# TODO: we should complain about this as a Liskov violation (incompatible override)
def __len__(self) -> Literal[42]:
return 42
reveal_type(len(FixedLengthSubclassWithDunderLenOverridden((1,)))) # revealed: Literal[42]
```
### Lists, sets and dictionaries
```py

View File

@@ -134,7 +134,6 @@ since these functions will never actually be called.
```py
from typing import TYPE_CHECKING
import typing
if TYPE_CHECKING:
def f() -> int: ...
@@ -200,9 +199,6 @@ if get_bool():
if TYPE_CHECKING:
if not TYPE_CHECKING:
def n() -> str: ...
if typing.TYPE_CHECKING:
def o() -> str: ...
```
## Conditional return type

View File

@@ -3,37 +3,27 @@
## `typing.TYPE_CHECKING`
This constant is `True` when in type-checking mode, `False` otherwise. The symbol is defined to be
`False` at runtime. In typeshed, it is annotated as `bool`.
`False` at runtime. In typeshed, it is annotated as `bool`. This test makes sure that we infer
`Literal[True]` for it anyways.
### Basic
```py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
type_checking = True
if not TYPE_CHECKING:
runtime = True
# type_checking is treated as unconditionally assigned.
reveal_type(type_checking) # revealed: Literal[True]
# error: [unresolved-reference]
reveal_type(runtime) # revealed: Unknown
```
### As module attribute
```py
import typing
if typing.TYPE_CHECKING:
type_checking = True
if not typing.TYPE_CHECKING:
runtime = True
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
reveal_type(typing.TYPE_CHECKING) # revealed: Literal[True]
```
reveal_type(type_checking) # revealed: Literal[True]
# error: [unresolved-reference]
reveal_type(runtime) # revealed: Unknown
### Aliased
Make sure that we still infer the correct type if the constant has been given a different name:
```py
from typing import TYPE_CHECKING as TC
reveal_type(TC) # revealed: Literal[True]
```
### `typing_extensions` re-export
@@ -43,14 +33,7 @@ This should behave in the same way as `typing.TYPE_CHECKING`:
```py
from typing_extensions import TYPE_CHECKING
if TYPE_CHECKING:
type_checking = True
if not TYPE_CHECKING:
runtime = True
reveal_type(type_checking) # revealed: Literal[True]
# error: [unresolved-reference]
reveal_type(runtime) # revealed: Unknown
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
```
## User-defined `TYPE_CHECKING`
@@ -63,7 +46,7 @@ type checkers, e.g. mypy and pyright.
```py
TYPE_CHECKING = False
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
if TYPE_CHECKING:
type_checking = True
if not TYPE_CHECKING:
@@ -78,11 +61,11 @@ reveal_type(runtime) # revealed: Unknown
### With a type annotation
We can also define `TYPE_CHECKING` with a type annotation. The type must be one to which `bool` can
be assigned.
be assigned. Even in this case, the type of `TYPE_CHECKING` is still inferred to be `Literal[True]`.
```py
TYPE_CHECKING: bool = False
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
if TYPE_CHECKING:
type_checking = True
if not TYPE_CHECKING:
@@ -101,21 +84,6 @@ reveal_type(runtime) # revealed: Unknown
TYPE_CHECKING = False
```
```py
from constants import TYPE_CHECKING
if TYPE_CHECKING:
type_checking = True
if not TYPE_CHECKING:
runtime = True
reveal_type(type_checking) # revealed: Literal[True]
# error: [unresolved-reference]
reveal_type(runtime) # revealed: Unknown
```
### Importing user-defined `TYPE_CHECKING` from stub
`stub.pyi`:
```pyi
@@ -125,16 +93,13 @@ TYPE_CHECKING: bool = ...
```
```py
from constants import TYPE_CHECKING
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
from stub import TYPE_CHECKING
if TYPE_CHECKING:
type_checking = True
if not TYPE_CHECKING:
runtime = True
reveal_type(type_checking) # revealed: Literal[True]
# error: [unresolved-reference]
reveal_type(runtime) # revealed: Unknown
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
```
### Invalid assignment to `TYPE_CHECKING`
@@ -157,14 +122,12 @@ TYPE_CHECKING: int = 1
# error: [invalid-type-checking-constant]
TYPE_CHECKING: str = "str"
# error: [invalid-assignment]
# error: [invalid-type-checking-constant]
TYPE_CHECKING: str = False
# error: [invalid-type-checking-constant]
TYPE_CHECKING: Literal[False] = False
# error: [invalid-assignment]
# error: [invalid-type-checking-constant]
TYPE_CHECKING: Literal[True] = False
```
@@ -177,7 +140,6 @@ from typing import Literal
# error: [invalid-type-checking-constant]
TYPE_CHECKING: str
# error: [invalid-assignment]
# error: [invalid-type-checking-constant]
TYPE_CHECKING: str = False

View File

@@ -84,6 +84,52 @@ def f():
x = "hello" # error: [invalid-assignment] "Object of type `Literal["hello"]` is not assignable to `int`"
```
## The types of `nonlocal` binding get unioned
Without a type declaration, we union the bindings in enclosing scopes to infer a type. But name
resolution stops at the closest binding that isn't declared `nonlocal`, and we ignore bindings
outside of that one:
```py
def a():
# This binding is shadowed in `b`, so we ignore it in inner scopes.
x = 1
def b():
x = 2
def c():
nonlocal x
x = 3
def d():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[3, 2]
x = 4
reveal_type(x) # revealed: Literal[4]
def e():
reveal_type(x) # revealed: Unknown | Literal[4, 3, 2]
```
However, currently the union of types that we build is incomplete. We walk parent scopes, but not
sibling scopes, child scopes, second-cousin-once-removed scopes, etc:
```py
def a():
x = 1
def b():
nonlocal x
x = 2
def c():
def d():
nonlocal x
x = 3
# TODO: This should include 2 and 3.
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Local variable bindings "look ahead" to any assignment in the current scope
The binding `x = 2` in `g` causes the earlier read of `x` to refer to `g`'s not-yet-initialized
@@ -390,3 +436,13 @@ def f():
nonlocal x
x = 1
```
## Narrowing nonlocal types to `Never` doesn't make them unbound
```py
def foo():
x: int = 1
def bar():
if isinstance(x, str):
reveal_type(x) # revealed: Never
```

View File

@@ -0,0 +1,93 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: deprecated.md - Tests for the `@deprecated` decorator - Introduction
mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import deprecated
2 |
3 | @deprecated("use OtherClass")
4 | def myfunc(): ...
5 |
6 | myfunc() # error: [deprecated] "use OtherClass"
7 | from typing_extensions import deprecated
8 |
9 | @deprecated("use BetterClass")
10 | class MyClass: ...
11 |
12 | MyClass() # error: [deprecated] "use BetterClass"
13 | from typing_extensions import deprecated
14 |
15 | class MyClass:
16 | @deprecated("use something else")
17 | def afunc(): ...
18 | @deprecated("don't use this!")
19 | def amethod(self): ...
20 |
21 | MyClass.afunc() # error: [deprecated] "use something else"
22 | MyClass().amethod() # error: [deprecated] "don't use this!"
```
# Diagnostics
```
warning[deprecated]: The function `myfunc` is deprecated
--> src/mdtest_snippet.py:6:1
|
4 | def myfunc(): ...
5 |
6 | myfunc() # error: [deprecated] "use OtherClass"
| ^^^^^^ use OtherClass
7 | from typing_extensions import deprecated
|
info: rule `deprecated` is enabled by default
```
```
warning[deprecated]: The class `MyClass` is deprecated
--> src/mdtest_snippet.py:12:1
|
10 | class MyClass: ...
11 |
12 | MyClass() # error: [deprecated] "use BetterClass"
| ^^^^^^^ use BetterClass
13 | from typing_extensions import deprecated
|
info: rule `deprecated` is enabled by default
```
```
warning[deprecated]: The function `afunc` is deprecated
--> src/mdtest_snippet.py:21:9
|
19 | def amethod(self): ...
20 |
21 | MyClass.afunc() # error: [deprecated] "use something else"
| ^^^^^ use something else
22 | MyClass().amethod() # error: [deprecated] "don't use this!"
|
info: rule `deprecated` is enabled by default
```
```
warning[deprecated]: The function `amethod` is deprecated
--> src/mdtest_snippet.py:22:11
|
21 | MyClass.afunc() # error: [deprecated] "use something else"
22 | MyClass().amethod() # error: [deprecated] "don't use this!"
| ^^^^^^^ don't use this!
|
info: rule `deprecated` is enabled by default
```

View File

@@ -0,0 +1,178 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: deprecated.md - Tests for the `@deprecated` decorator - Syntax
mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import deprecated
2 |
3 | @deprecated # error: [invalid-argument-type] "LiteralString"
4 | def invalid_deco(): ...
5 |
6 | invalid_deco() # error: [missing-argument]
7 | from typing_extensions import deprecated
8 |
9 | @deprecated() # error: [missing-argument] "message"
10 | def invalid_deco(): ...
11 |
12 | invalid_deco()
13 | from typing_extensions import deprecated
14 |
15 | x = "message"
16 |
17 | @deprecated(x)
18 | def invalid_deco(): ...
19 |
20 | invalid_deco() # error: [deprecated] "message"
21 | from typing_extensions import deprecated, LiteralString
22 |
23 | def opaque() -> LiteralString:
24 | return "message"
25 |
26 | @deprecated(opaque())
27 | def valid_deco(): ...
28 |
29 | valid_deco() # error: [deprecated]
30 | from typing_extensions import deprecated
31 |
32 | def opaque() -> str:
33 | return "message"
34 |
35 | @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString"
36 | def dubious_deco(): ...
37 |
38 | dubious_deco()
39 | from typing_extensions import deprecated
40 |
41 | @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf"
42 | def invalid_deco(): ...
43 |
44 | invalid_deco()
45 | from typing_extensions import deprecated
46 |
47 | @deprecated("some message", category=DeprecationWarning, stacklevel=1)
48 | def valid_deco(): ...
49 |
50 | valid_deco() # error: [deprecated] "some message"
```
# Diagnostics
```
error[invalid-argument-type]: Argument to class `deprecated` is incorrect
--> src/mdtest_snippet.py:3:1
|
1 | from typing_extensions import deprecated
2 |
3 | @deprecated # error: [invalid-argument-type] "LiteralString"
| ^^^^^^^^^^^ Expected `LiteralString`, found `def invalid_deco() -> Unknown`
4 | def invalid_deco(): ...
|
info: rule `invalid-argument-type` is enabled by default
```
```
error[missing-argument]: No argument provided for required parameter `arg` of bound method `__call__`
--> src/mdtest_snippet.py:6:1
|
4 | def invalid_deco(): ...
5 |
6 | invalid_deco() # error: [missing-argument]
| ^^^^^^^^^^^^^^
7 | from typing_extensions import deprecated
|
info: rule `missing-argument` is enabled by default
```
```
error[missing-argument]: No argument provided for required parameter `message` of class `deprecated`
--> src/mdtest_snippet.py:9:2
|
7 | from typing_extensions import deprecated
8 |
9 | @deprecated() # error: [missing-argument] "message"
| ^^^^^^^^^^^^
10 | def invalid_deco(): ...
|
info: rule `missing-argument` is enabled by default
```
```
warning[deprecated]: The function `invalid_deco` is deprecated
--> src/mdtest_snippet.py:20:1
|
18 | def invalid_deco(): ...
19 |
20 | invalid_deco() # error: [deprecated] "message"
| ^^^^^^^^^^^^ message
21 | from typing_extensions import deprecated, LiteralString
|
info: rule `deprecated` is enabled by default
```
```
warning[deprecated]: The function `valid_deco` is deprecated
--> src/mdtest_snippet.py:29:1
|
27 | def valid_deco(): ...
28 |
29 | valid_deco() # error: [deprecated]
| ^^^^^^^^^^
30 | from typing_extensions import deprecated
|
info: rule `deprecated` is enabled by default
```
```
error[invalid-argument-type]: Argument to class `deprecated` is incorrect
--> src/mdtest_snippet.py:35:13
|
33 | return "message"
34 |
35 | @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString"
| ^^^^^^^^ Expected `LiteralString`, found `str`
36 | def dubious_deco(): ...
|
info: rule `invalid-argument-type` is enabled by default
```
```
error[unknown-argument]: Argument `dsfsdf` does not match any known parameter of class `deprecated`
--> src/mdtest_snippet.py:41:29
|
39 | from typing_extensions import deprecated
40 |
41 | @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf"
| ^^^^^^^^^^^^^^^^^
42 | def invalid_deco(): ...
|
info: rule `unknown-argument` is enabled by default
```
```
warning[deprecated]: The function `valid_deco` is deprecated
--> src/mdtest_snippet.py:50:1
|
48 | def valid_deco(): ...
49 |
50 | valid_deco() # error: [deprecated] "some message"
| ^^^^^^^^^^ some message
|
info: rule `deprecated` is enabled by default
```

View File

@@ -29,16 +29,16 @@ error[invalid-argument-type]: Argument to function `loads` is incorrect
| ^ Expected `str | bytes | bytearray`, found `Literal[5]`
|
info: Function defined here
--> stdlib/json/__init__.pyi:219:5
--> stdlib/json/__init__.pyi:218:5
|
217 | """
218 |
219 | def loads(
216 | """
217 |
218 | def loads(
| ^^^^^
220 | s: str | bytes | bytearray,
219 | s: str | bytes | bytearray,
| -------------------------- Parameter declared here
221 | *,
222 | cls: type[JSONDecoder] | None = None,
220 | *,
221 | cls: type[JSONDecoder] | None = None,
|
info: rule `invalid-argument-type` is enabled by default

View File

@@ -36,7 +36,7 @@ error[invalid-syntax]
--> src/mdtest_snippet.py:6:19
|
4 | async def f():
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (synt...
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax…
6 | return {n: [x async for x in elements(n)] for n in range(3)}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
7 | async def test():

View File

@@ -41,7 +41,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/with/sync.md
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
--> src/mdtest_snippet.py:6:6
|
5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and...
5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `…
6 | with Manager():
| ^^^^^^^^^
7 | ...
@@ -57,7 +57,7 @@ info: rule `invalid-context-manager` is enabled by default
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
--> src/mdtest_snippet.py:13:6
|
12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an...
12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and …
13 | with Manager():
| ^^^^^^^^^
14 | ...
@@ -73,7 +73,7 @@ info: rule `invalid-context-manager` is enabled by default
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
--> src/mdtest_snippet.py:20:6
|
19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an...
19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and …
20 | with Manager():
| ^^^^^^^^^
21 | ...

View File

@@ -154,6 +154,7 @@ the expression is not of statically known truthiness.
```py
from ty_extensions import static_assert
from typing import TYPE_CHECKING
import sys
static_assert(True)
@@ -173,6 +174,8 @@ static_assert("d" in "abc") # error: "Static assertion error: argument evaluate
n = None
static_assert(n is None)
static_assert(TYPE_CHECKING)
static_assert(sys.version_info >= (3, 6))
```

View File

@@ -551,6 +551,11 @@ static_assert(is_subtype_of(Never, AlwaysFalsy))
### `AlwaysTruthy` and `AlwaysFalsy`
```toml
[environment]
python-version = "3.11"
```
```py
from ty_extensions import AlwaysTruthy, AlwaysFalsy, Intersection, Not, is_subtype_of, static_assert
from typing_extensions import Literal, LiteralString
@@ -588,6 +593,30 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]],
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[AlwaysFalsy]))
# error: [static-assert-error]
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy]))
class Length2TupleSubclass(tuple[int, str]): ...
static_assert(is_subtype_of(Length2TupleSubclass, AlwaysTruthy))
class EmptyTupleSubclass(tuple[()]): ...
static_assert(is_subtype_of(EmptyTupleSubclass, AlwaysFalsy))
class TupleSubclassWithAtLeastLength2(tuple[int, *tuple[str, ...], bytes]): ...
static_assert(is_subtype_of(TupleSubclassWithAtLeastLength2, AlwaysTruthy))
class UnknownLength(tuple[int, ...]): ...
static_assert(not is_subtype_of(UnknownLength, AlwaysTruthy))
static_assert(not is_subtype_of(UnknownLength, AlwaysFalsy))
class Invalid(tuple[int, str]):
# TODO: we should emit an error here (Liskov violation)
def __bool__(self) -> Literal[False]:
return False
static_assert(is_subtype_of(Invalid, AlwaysFalsy))
```
### `TypeGuard` and `TypeIs`

View File

@@ -170,26 +170,37 @@ Assignments to attributes qualified with `Final` are also not allowed:
```py
from typing import Final
class C:
FINAL_A: Final[int] = 1
FINAL_B: Final = 1
class Meta(type):
META_FINAL_A: Final[int] = 1
META_FINAL_B: Final = 1
class C(metaclass=Meta):
CLASS_FINAL_A: Final[int] = 1
CLASS_FINAL_B: Final = 1
def __init__(self):
self.FINAL_C: Final[int] = 1
self.FINAL_D: Final = 1
self.INSTANCE_FINAL_A: Final[int] = 1
self.INSTANCE_FINAL_B: Final = 1
# TODO: these should be errors (that mention `Final`)
C.FINAL_A = 2
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
C.FINAL_B = 2
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type `<class 'C'>`"
C.META_FINAL_A = 2
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_B` on type `<class 'C'>`"
C.META_FINAL_B = 2
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `<class 'C'>`"
C.CLASS_FINAL_A = 2
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `<class 'C'>`"
C.CLASS_FINAL_B = 2
# TODO: these should be errors (that mention `Final`)
c = C()
c.FINAL_A = 2
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
c.FINAL_B = 2
c.FINAL_C = 2
c.FINAL_D = 2
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `C`"
c.CLASS_FINAL_A = 2
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `C`"
c.CLASS_FINAL_B = 2
# TODO: this should be an error
c.INSTANCE_FINAL_A = 2
# TODO: this should be an error
c.INSTANCE_FINAL_B = 2
```
## Mutability

View File

@@ -5,7 +5,8 @@ use ruff_db::files::File;
/// Database giving access to semantic information about a Python program.
#[salsa::db]
pub trait Db: SourceDb {
fn is_file_open(&self, file: File) -> bool;
/// Returns `true` if the file should be checked.
fn should_check_file(&self, file: File) -> bool;
/// Resolves the rule selection for a given file.
fn rule_selection(&self, file: File) -> &RuleSelection;
@@ -114,7 +115,7 @@ pub(crate) mod tests {
#[salsa::db]
impl Db for TestDb {
fn is_file_open(&self, file: File) -> bool {
fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path()
}

View File

@@ -17,8 +17,10 @@ pub use program::{
pub use python_platform::PythonPlatform;
pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use types::definitions_for_name;
pub use types::ide_support::ResolvedDefinition;
pub use types::ide_support::{
ResolvedDefinition, definitions_for_attribute, definitions_for_imported_symbol,
definitions_for_name,
};
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
pub mod ast_node_ref;

View File

@@ -17,6 +17,7 @@ pub struct Module {
inner: Arc<ModuleInner>,
}
#[salsa::tracked]
impl Module {
pub(crate) fn file_module(
name: ModuleName,
@@ -97,11 +98,16 @@ impl Module {
///
/// The names returned correspond to the "base" name of the module.
/// That is, `{self.name}.{basename}` should give the full module name.
pub fn all_submodules(&self, db: &dyn Db) -> Vec<Name> {
self.all_submodules_inner(db).unwrap_or_default()
pub fn all_submodules<'db>(&self, db: &'db dyn Db) -> &'db [Name] {
self.clone()
.all_submodules_inner(db, ())
.as_deref()
.unwrap_or_default()
}
fn all_submodules_inner(&self, db: &dyn Db) -> Option<Vec<Name>> {
#[allow(clippy::ref_option, clippy::used_underscore_binding)]
#[salsa::tracked(returns(ref))]
fn all_submodules_inner(self, db: &dyn Db, _dummy: ()) -> Option<Vec<Name>> {
fn is_submodule(
is_dir: bool,
is_file: bool,
@@ -136,32 +142,42 @@ impl Module {
);
Some(match path.parent()? {
SystemOrVendoredPathRef::System(parent_directory) => db
.system()
.read_directory(parent_directory)
.inspect_err(|err| {
tracing::debug!(
"Failed to read {parent_directory:?} when looking for \
its possible submodules: {err}"
);
})
.ok()?
.flatten()
.filter(|entry| {
let ty = entry.file_type();
let path = entry.path();
is_submodule(
ty.is_directory(),
ty.is_file(),
path.file_name(),
path.extension(),
)
})
.filter_map(|entry| {
let stem = entry.path().file_stem()?;
is_identifier(stem).then(|| Name::from(stem))
})
.collect(),
SystemOrVendoredPathRef::System(parent_directory) => {
// Read the revision on the corresponding file root to
// register an explicit dependency on this directory
// tree. When the revision gets bumped, the cache
// that Salsa creates does for this routine will be
// invalidated.
if let Some(root) = db.files().root(db, parent_directory) {
let _ = root.revision(db);
}
db.system()
.read_directory(parent_directory)
.inspect_err(|err| {
tracing::debug!(
"Failed to read {parent_directory:?} when looking for \
its possible submodules: {err}"
);
})
.ok()?
.flatten()
.filter(|entry| {
let ty = entry.file_type();
let path = entry.path();
is_submodule(
ty.is_directory(),
ty.is_file(),
path.file_name(),
path.extension(),
)
})
.filter_map(|entry| {
let stem = entry.path().file_stem()?;
is_identifier(stem).then(|| Name::from(stem))
})
.collect()
}
SystemOrVendoredPathRef::Vendored(parent_directory) => db
.vendored()
.read_directory(parent_directory)
@@ -259,6 +275,7 @@ pub enum KnownModule {
UnittestMock,
#[cfg(test)]
Uuid,
Warnings,
}
impl KnownModule {
@@ -278,6 +295,7 @@ impl KnownModule {
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
Self::TyExtensions => "ty_extensions",
Self::ImportLib => "importlib",
Self::Warnings => "warnings",
#[cfg(test)]
Self::UnittestMock => "unittest.mock",
#[cfg(test)]

View File

@@ -754,10 +754,14 @@ fn place_by_id<'db>(
// a diagnostic if we see it being modified externally. In type inference, we
// can assign a "narrow" type to it even if it is not *declared*. This means, we
// do not have to call [`widen_type_for_undeclared_public_symbol`].
//
// `TYPE_CHECKING` is a special variable that should only be assigned `False`
// at runtime, but is always considered `True` in type checking.
// See mdtest/known_constants.md#user-defined-type_checking for details.
let is_considered_non_modifiable = place_table(db, scope)
.place_expr(place_id)
.expr
.is_name_and(|name| matches!(name, "__slots__"));
.is_name_and(|name| matches!(name, "__slots__" | "TYPE_CHECKING"));
if scope.file(db).is_stub(db) {
// We generally trust module-level undeclared places in stubs and do not union

View File

@@ -549,20 +549,14 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
}
fn build_predicate(&mut self, predicate_node: &ast::Expr) -> PredicateOrLiteral<'db> {
// Some commonly used test expressions are eagerly evaluated as `true` or `false` here for
// performance reasons. This list does not need to be exhaustive. More complex expressions
// will still evaluate to the correct value during type-checking. (The one exception is
// `TYPE_CHECKING`; we need to detect it here in order to handle it correctly in
// conditions; in type inference it will resolve to its runtime value.)
// Some commonly used test expressions are eagerly evaluated as `true`
// or `false` here for performance reasons. This list does not need to
// be exhaustive. More complex expressions will still evaluate to the
// correct value during type-checking.
fn resolve_to_literal(node: &ast::Expr) -> Option<bool> {
match node {
ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => Some(*value),
ast::Expr::Name(ast::ExprName { id, .. }) if id == "TYPE_CHECKING" => Some(true),
ast::Expr::Attribute(ast::ExprAttribute { attr, .. })
if attr == "TYPE_CHECKING" =>
{
Some(true)
}
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(n),
..
@@ -2000,8 +1994,26 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
walk_stmt(self, stmt);
for target in targets {
if let Ok(target) = PlaceExpr::try_from(target) {
let is_name = target.is_name();
let place_id = self.add_place(PlaceExprWithFlags::new(target));
self.current_place_table_mut().mark_place_used(place_id);
let place_table = self.current_place_table_mut();
if is_name {
// `del x` behaves like an assignment in that it forces all references
// to `x` in the current scope (including *prior* references) to refer
// to the current scope's binding (unless `x` is declared `global` or
// `nonlocal`). For example, this is an UnboundLocalError at runtime:
//
// ```py
// x = 1
// def foo():
// print(x) # can't refer to global `x`
// if False:
// del x
// foo()
// ```
place_table.mark_place_bound(place_id);
}
place_table.mark_place_used(place_id);
self.delete_binding(place_id);
}
}
@@ -2528,7 +2540,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
}
fn report_semantic_error(&self, error: SemanticSyntaxError) {
if self.db.is_file_open(self.file) {
if self.db.should_check_file(self.file) {
self.semantic_syntax_errors.borrow_mut().push(error);
}
}
@@ -2759,12 +2771,14 @@ impl ExpressionsScopeMapBuilder {
/// Returns if the expression is a `TYPE_CHECKING` expression.
fn is_if_type_checking(expr: &ast::Expr) -> bool {
matches!(expr, ast::Expr::Name(ast::ExprName { id, .. }) if id == "TYPE_CHECKING")
|| matches!(expr, ast::Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "TYPE_CHECKING")
}
/// Returns if the expression is a `not TYPE_CHECKING` expression.
fn is_if_not_type_checking(expr: &ast::Expr) -> bool {
matches!(expr, ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, .. }) if *op == ruff_python_ast::UnaryOp::Not
&& is_if_type_checking(operand)
&& matches!(
&**operand,
ast::Expr::Name(ast::ExprName { id, .. }) if id == "TYPE_CHECKING"
)
)
}

View File

@@ -107,7 +107,7 @@ pub struct Definitions<'db> {
impl<'db> Definitions<'db> {
pub(crate) fn single(definition: Definition<'db>) -> Self {
Self {
definitions: smallvec::smallvec![definition],
definitions: smallvec::smallvec_inline![definition],
}
}

View File

@@ -10,7 +10,7 @@ use ruff_index::{IndexVec, newtype_index};
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use rustc_hash::FxHasher;
use smallvec::{SmallVec, smallvec};
use smallvec::SmallVec;
use crate::Db;
use crate::ast_node_ref::AstNodeRef;
@@ -162,10 +162,10 @@ impl TryFrom<ast::ExprRef<'_>> for PlaceExpr {
}
impl PlaceExpr {
pub(crate) fn name(name: Name) -> Self {
pub(crate) const fn name(name: Name) -> Self {
Self {
root_name: name,
sub_segments: smallvec![],
sub_segments: SmallVec::new_const(),
}
}

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