Compare commits

..

50 Commits

Author SHA1 Message Date
Douglas Creager
9eff2734bb wip: subscript always via __getitem__ 2025-03-27 14:19:48 -04:00
Micha Reiser
640d821108 [red-knot] reveal-type should return the revaled type (#17007)
## Summary

Return the revealed-type from the monkey-patched `revale_type`
implementation to
preserve the identity behavior.

This PR also isolates different script runs by assigning a different
`globals` dict for each script-run. See
https://github.com/pyodide/pyodide/issues/703
2025-03-27 03:04:57 +00:00
Micha Reiser
43ca85a351 [red-knot] Add run panel (#17002)
## Summary

This PR adds a new secondary panel to the red knot playground that
allows running the python code (current file) with
[pyodide](https://pyodide.org/en/stable/index.html) (currently Python
3.12 only).



## Test Plan


https://github.com/user-attachments/assets/7bda8ef7-19fb-4c2f-8e62-8e49a1416be1
2025-03-26 21:32:07 +00:00
Micha Reiser
338fed98a4 [red-knot] Use React suspense to show loading spinner (#16986)
## Summary

Use React's suspense feature to show a loading spinner while the WASM
module is initializing.
2025-03-26 17:56:14 +00:00
Brent Westbrook
d70a3e6753 [syntax-errors] Multiple assignments in case pattern (#16957)
Summary
--

This PR detects multiple assignments to the same name in `case` patterns
by recursively visiting each pattern.

Test Plan
--

New inline tests.
2025-03-26 13:02:42 -04:00
Brent Westbrook
5697d21fca [syntax-errors] Irrefutable case pattern before final case (#16905)
Summary
--

Detects irrefutable `match` cases before the final case using a modified
version
of the existing `Pattern::is_irrefutable` method from the AST crate. The
modified method helps to retrieve a more precise diagnostic range to
match what
Python 3.13 shows in the REPL.

Test Plan
--

New inline tests, as well as some updates to existing tests that had
irrefutable
patterns before the last block.
2025-03-26 12:27:16 -04:00
Wei Lee
58350ec93b [airflow] refactor: remove unnecessary Some in check_method, check_class_attribute (AIR302) (#16975)
## Summary

remove unnecessary `Some`

## Test Plan

It's a refactoring change. Existing test cases won't be affected
2025-03-26 12:17:34 -04:00
Matthew Mckee
aae4d0f3eb [red-knot] A FunctionType can be a subtype of Callable (but never the other way around) (#16970)
## Summary

Partially fixes #16953

## Test Plan

Update is_subtype_of.md

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-03-25 22:04:34 +00:00
renovate[bot]
807fce8069 Update dependency vite to v6.2.3 (#16972)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [vite](https://vite.dev)
([source](https://redirect.github.com/vitejs/vite/tree/HEAD/packages/vite))
| [`6.2.2` ->
`6.2.3`](https://renovatebot.com/diffs/npm/vite/6.2.2/6.2.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vite/6.2.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vite/6.2.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vite/6.2.2/6.2.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/6.2.2/6.2.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

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

### GitHub Vulnerability Alerts

####
[CVE-2025-30208](https://redirect.github.com/vitejs/vite/security/advisories/GHSA-x574-m823-4x7w)

### Summary
The contents of arbitrary files can be returned to the browser.

### Impact
Only apps explicitly exposing the Vite dev server to the network (using
`--host` or [`server.host` config
option](https://vitejs.dev/config/server-options.html#server-host)) are
affected.

### Details
`@fs` denies access to files outside of Vite serving allow list. Adding
`?raw??` or `?import&raw??` to the URL bypasses this limitation and
returns the file content if it exists. This bypass exists because
trailing separators such as `?` are removed in several places, but are
not accounted for in query string regexes.

### PoC
```bash
$ npm create vite@latest
$ cd vite-project/
$ npm install
$ npm run dev

$ echo "top secret content" > /tmp/secret.txt

# expected behaviour
$ curl "http://localhost:5173/@&#8203;fs/tmp/secret.txt"

    <body>
      <h1>403 Restricted</h1>
      <p>The request url &quot;/tmp/secret.txt&quot; is outside of Vite serving allow list.

# security bypassed
$ curl "http://localhost:5173/@&#8203;fs/tmp/secret.txt?import&raw??"
export default "top secret content\n"
//# sourceMappingURL=data:application/json;base64,eyJ2...
```

---

### Release Notes

<details>
<summary>vitejs/vite (vite)</summary>

###
[`v6.2.3`](https://redirect.github.com/vitejs/vite/releases/tag/v6.2.3)

[Compare
Source](https://redirect.github.com/vitejs/vite/compare/v6.2.2...v6.2.3)

Please refer to
[CHANGELOG.md](https://redirect.github.com/vitejs/vite/blob/v6.2.3/packages/vite/CHANGELOG.md)
for details.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no
schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCIsInNlY3VyaXR5Il19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-25 20:17:40 +00:00
Micha Reiser
8d16a5c8c9 [red-knot] Use web-time instead of FileTime::now (#16967)
## Summary

`std::time::now` isn't available on `wasm32-unknown-unknown` but it is
used by `FileTime::now`.

This PR replaces the usages of `FileTime::now` with a target specific
helper function that we already had in the memory file system.
Fixes https://github.com/astral-sh/ruff/issues/16966

## Test Plan

Tested that the playground no longer crash when adding an extra-path
2025-03-25 13:03:30 +00:00
Alex Waygood
4975c2f027 [red-knot] Fix panic on cyclic * imports (#16958)
## Summary

Further work towards https://github.com/astral-sh/ruff/issues/14169.

We currently panic on encountering cyclic `*` imports. This is easily
fixed using fixpoint iteration.

## Test Plan

Added a test that panics on `main`, but passes with this PR
2025-03-24 18:23:02 +00:00
Dhruv Manilawala
dd5b02aaa2 [red-knot] Fix gradual equivalence for callable types (#16887)
## Summary

As mentioned in
https://github.com/astral-sh/ruff/pull/16698#discussion_r2004920075,
part of #15382, this PR updates the `is_gradual_equivalent_to`
implementation between callable types to be similar to
`is_equivalent_to` and checks other attributes of parameters like name,
optionality, and parameter kind.

## Test Plan

Expand the existing test cases to consider other properties but not all
similar to how the tests are structured for subtyping and assignability.
2025-03-24 23:46:06 +05:30
Aleksei Latyshev
68ea2b8b5b [red-knot] simplify "removing" in UnionBuilder::add (#16947)
## Summary

Simplify "removing" in UnionBuilder::add
It's now O(m) instead of O(n + m) and easier to read.

## Test Plan

cargo test (incl. mdtest)
2025-03-24 14:04:03 -04:00
Alex Waygood
e87fee4b3b [red-knot] Add initial support for * imports (#16923)
## Summary

This PR adds initial support for `*` imports to red-knot. The approach
is to implement a standalone query, called from semantic indexing, that
visits the module referenced by the `*` import and collects all
global-scope public names that will be imported by the `*` import. The
`SemanticIndexBuilder` then adds separate definitions for each of these
names, all keyed to the same `ast::Alias` node that represents the `*`
import.

There are many pieces of `*`-import semantics that are still yet to be
done, even with this PR:
- This PR does not attempt to implement any of the semantics to do with
`__all__`. (If a module defines `__all__`, then only the symbols
included in `__all__` are imported, _not_ all public global-scope
symbols.
- With the logic implemented in this PR as it currently stands, we
sometimes incorrectly consider a symbol bound even though it is defined
in a branch that is statically known to be dead code, e.g. (assuming the
target Python version is set to 3.11):

  ```py
  # a.py

  import sys

  if sys.version_info < (3, 10):
      class Foo: ...

  ```

  ```py
  # b.py

  from a import *

  print(Foo)  # this is unbound at runtime on 3.11,
# but we currently consider it bound with the logic in this PR
  ```

Implementing these features is important, but is for now deferred to
followup PRs.

Many thanks to @ntBre, who contributed to this PR in a pairing session
on Friday!

## Test Plan

Assertions in existing mdtests are adjusted, and several new ones are
added.
2025-03-24 17:15:58 +00:00
Micha Reiser
cba197e3c5 [red-knot] Default playground to Python 3.13 for real (#16956)
## Summary

Default to 3.13 for good. 

I incorrectly used `workspace.updateOptions` instead of `updateOptions`
where the latter has a fallback.

## Test Plan

```py
import os

import sys

reveal_type(sys.version_info.minor)
```

reveals 13 on initial page load
2025-03-24 16:40:49 +00:00
Alex Waygood
66d0cf2a72 [red-knot] Add more tests for * imports (#16955)
## Summary

This PR separates out the entirely new tests from
https://github.com/astral-sh/ruff/pull/16923 into a standalone PR. I'll
rebase https://github.com/astral-sh/ruff/pull/16923 on top of this
branch.

The reasons for separating it out are:
- It should make it clearer to see in
<https://github.com/astral-sh/ruff/pull/16923> exactly how the
functionality is changing (we can see the assertions in the tests
_change_, which isn't so obvious if the tests are entirely new)
- The diff on <https://github.com/astral-sh/ruff/pull/16923> is getting
pretty big; this should reduce the diff on that PR somewhat
- These tests seem useful in and of themselves, so even if we need to do
a wholesale revert of <https://github.com/astral-sh/ruff/pull/16923> for
whatever reason, it'll be nice to keep the tests

## Test Plan

`cargo test -p red_knot_python_semantic`
2025-03-24 16:39:16 +00:00
Micha Reiser
85b7f808e1 [red-knot] Default playground to Python 3.13 (#16952)
## Summary

Default playground to Python 3.13 if there's no setting present. Fix
errors when a setting was added / removed.
2025-03-24 15:54:54 +00:00
renovate[bot]
3a97bdf689 Update Rust crate getrandom to v0.3.2 (#16939)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [getrandom](https://redirect.github.com/rust-random/getrandom) |
workspace.dependencies | patch | `0.3.1` -> `0.3.2` |

---

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

---

### Release Notes

<details>
<summary>rust-random/getrandom (getrandom)</summary>

###
[`v0.3.2`](https://redirect.github.com/rust-random/getrandom/blob/HEAD/CHANGELOG.md#032---2025-03-17)

[Compare
Source](https://redirect.github.com/rust-random/getrandom/compare/v0.3.1...v0.3.2)

##### Added

-   `efi_rng` opt-in backend [#&#8203;570]
-   `linux_raw` opt-in backend [#&#8203;572]
-   `.cargo/config.toml` example in the crate-level docs [#&#8203;591]
- `getrandom_test_linux_without_fallback` configuration flag to test
that file fallback
is not triggered in the `linux_android_with_fallback` backend
[#&#8203;605]
-   Built-in support for `*-linux-none` targets [#&#8203;618]
-   Cygwin support [#&#8203;626]

##### Changed

-   Update `wasi` dependency to v0.14 [#&#8203;594]
-   Add `#[inline]` attribute to the inner functions [#&#8203;596]
- Update WASI and Emscripten links in the crate-level docs [#&#8203;597]
- Do not use `dlsym` on MUSL targets in the
`linux_android_with_fallback` backend [#&#8203;602]
- Remove `linux_android.rs` and use `getrandom.rs` instead [#&#8203;603]
- Always use `RtlGenRandom` on Windows targets when compiling with
pre-1.78 Rust [#&#8203;610]
-   Internal representation of the `Error` type [#&#8203;614]
- Remove `windows-targets` dependency and use [`raw-dylib`][raw-dylib]
directly [#&#8203;627]

##### Removed

- `Error::INTERNAL_START` and `Error::CUSTOM_START` associated constants
[#&#8203;614]

[#&#8203;570]:
https://redirect.github.com/rust-random/getrandom/pull/570

[#&#8203;572]:
https://redirect.github.com/rust-random/getrandom/pull/572

[#&#8203;591]:
https://redirect.github.com/rust-random/getrandom/pull/591

[#&#8203;594]:
https://redirect.github.com/rust-random/getrandom/pull/594

[#&#8203;596]:
https://redirect.github.com/rust-random/getrandom/pull/596

[#&#8203;597]:
https://redirect.github.com/rust-random/getrandom/pull/597

[#&#8203;602]:
https://redirect.github.com/rust-random/getrandom/pull/602

[#&#8203;603]:
https://redirect.github.com/rust-random/getrandom/pull/603

[#&#8203;605]:
https://redirect.github.com/rust-random/getrandom/pull/605

[#&#8203;610]:
https://redirect.github.com/rust-random/getrandom/pull/610

[#&#8203;614]:
https://redirect.github.com/rust-random/getrandom/pull/614

[#&#8203;618]:
https://redirect.github.com/rust-random/getrandom/pull/618

[#&#8203;626]:
https://redirect.github.com/rust-random/getrandom/pull/626

[#&#8203;627]:
https://redirect.github.com/rust-random/getrandom/pull/627

[`raw-dylib`]:
https://doc.rust-lang.org/reference/items/external-blocks.html?highlight=link#dylib-versus-raw-dylib

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 15:54:43 +00:00
renovate[bot]
1bee3994aa Update PyO3/maturin-action digest to 22fe573 (#16932)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [PyO3/maturin-action](https://redirect.github.com/PyO3/maturin-action)
| action | digest | `36db840` -> `22fe573` |

---

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

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 15:53:59 +00:00
Alex Waygood
888a910925 [red-knot] Demote the negation_reverses_subtype_order test back to flaky (#16951)
Fixes #16913. See my analysis in the issue for the rationale
2025-03-24 11:37:03 -04:00
Wei Lee
581b7005dc [airflow] refactor: combine similar case condition (AIR302) (#16944)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

combine similar case condition in AIR302 

## Test Plan

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

nothing should be changed. existing test case should already cover it
2025-03-24 15:36:33 +00:00
renovate[bot]
b442ba440f Update astral-sh/setup-uv digest to 2269511 (#16937)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | digest | `f94ec6b` -> `2269511` |

---

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

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 15:32:27 +00:00
renovate[bot]
5aba72cdbd Update taiki-e/install-action digest to 914ac1e (#16938)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[taiki-e/install-action](https://redirect.github.com/taiki-e/install-action)
| action | digest | `2c41309` -> `914ac1e` |

---

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

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 15:31:24 +00:00
Brent Westbrook
2711e08eb8 [syntax-errors] Fix false positive for parenthesized tuple index (#16948)
Summary
--

Fixes #16943 by checking if the tuple is not parenthesized before
emitting an error.

Test Plan
--

New inline test based on the initial report
2025-03-24 10:34:38 -04:00
Micha Reiser
f5cdf23545 [red-knot] Add settings support to playground (#16929)
## Summary

This PR extends the Red Knot playground by adding configuration support
by adding a `knot.json` file.

<img width="1679" alt="Screenshot 2025-03-23 at 21 12 16"
src="https://github.com/user-attachments/assets/81ff1588-a07a-4847-97d8-61250aa2feda"
/>
2025-03-24 01:38:48 +00:00
renovate[bot]
d98222cd14 Update actions/cache digest to 5a3ec84 (#16934)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/cache](https://redirect.github.com/actions/cache) | action |
digest | `d4323d4` -> `5a3ec84` |

---

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

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 01:38:32 +00:00
renovate[bot]
f7b9089cb8 Update actions/upload-artifact digest to ea165f8 (#16936)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/upload-artifact](https://redirect.github.com/actions/upload-artifact)
| action | digest | `4cec3d8` -> `ea165f8` |

---

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

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 01:27:05 +00:00
renovate[bot]
dfebc1cfe4 Update Rust crate tempfile to v3.19.1 (#16941)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tempfile](https://stebalien.com/projects/tempfile-rs/)
([source](https://redirect.github.com/Stebalien/tempfile)) |
workspace.dependencies | patch | `3.19.0` -> `3.19.1` |

---

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

---

### Release Notes

<details>
<summary>Stebalien/tempfile (tempfile)</summary>

###
[`v3.19.1`](https://redirect.github.com/Stebalien/tempfile/blob/HEAD/CHANGELOG.md#3191)

[Compare
Source](https://redirect.github.com/Stebalien/tempfile/compare/v3.19.0...v3.19.1)

- Don't unlink temporary files immediately on Windows (fixes
[#&#8203;339](https://redirect.github.com/Stebalien/tempfile/issues/339)).
Unfortunately, this seemed to corrupt the file object (possibly a
Windows kernel bug) in rare cases and isn't strictly speaking necessary.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 01:26:39 +00:00
renovate[bot]
7e1484a9b1 Update Rust crate mimalloc to v0.1.44 (#16940)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mimalloc](https://redirect.github.com/purpleprotocol/mimalloc_rust) |
workspace.dependencies | patch | `0.1.43` -> `0.1.44` |

---

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

---

### Release Notes

<details>
<summary>purpleprotocol/mimalloc_rust (mimalloc)</summary>

###
[`v0.1.44`](https://redirect.github.com/purpleprotocol/mimalloc_rust/releases/tag/v0.1.44):
Version 0.1.44

[Compare
Source](https://redirect.github.com/purpleprotocol/mimalloc_rust/compare/v0.1.43...v0.1.44)

##### Changes

-   Mimalloc v2.2.2

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 01:26:02 +00:00
renovate[bot]
187cac56bd Update actions/download-artifact digest to 95815c3 (#16935)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/download-artifact](https://redirect.github.com/actions/download-artifact)
| action | digest | `cc20338` -> `95815c3` |

---

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

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 01:24:41 +00:00
renovate[bot]
890f79c4ab Update Swatinem/rust-cache digest to 9d47c6a (#16933)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [Swatinem/rust-cache](https://redirect.github.com/Swatinem/rust-cache)
| action | digest | `f0deed1` -> `9d47c6a` |

---

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

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 01:21:42 +00:00
John Stilley
3899f7156f Fixing more spelling errors (#16926)
## Summary

Here I fix the last English spelling errors I could find in the repo.

Again, I am trying not to touch variable/function names, or anything
that might be misspelled in the API. The goal is to make this PR safe
and easy to merge.

## Test Plan

I have run all the unit tests. Though, again, all of the changes I make
here are to docs and docstrings. I make no code changes, which I believe
should greatly mitigate the testing concerns.
2025-03-23 10:55:14 -07:00
InSync
902d86e79e [red-knot] Do not emit invalid-return-type for abstract functions (#16900)
## Summary

Resolves #16895.

`abstractmethod` is now a `KnownFunction`. When a function is decorated
by `abstractmethod` or when the parent class inherits directly from
`Protocol`, `invalid-return-type` won't be emitted for that function.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@oddbird.net>
2025-03-23 17:51:10 +00:00
Daniel Wilton
9fe89ddfba [refurb] Document why UserDict, UserList, UserString are preferred over dict, list, str (FURB189) (#16927)
## Summary

This PR addresses docs issue
https://github.com/astral-sh/ruff/issues/14328.
2025-03-23 13:24:39 -04:00
Matthew Mckee
08a0995108 [red-knot] Disambiguate display for intersection types (#16914)
## Summary

Fixes #16912 

Create a new type `DisplayMaybeParenthesizedType` that is now used in
Union and Intersection display

## Test Plan

Update callable annotations
2025-03-23 07:18:30 -07:00
InSync
2d892bc9f7 Fix typos (#16908)
## Summary

The noun is spelled "descend<strong><em>a</em></strong>nt" and the
adjective "descend<strong><em>e</em></strong>nt".

## Test Plan

[From the English
Wiktionary](https://en.wiktionary.org/wiki/descendent#Usage_notes):

> The adjective, "descending from a biological ancestor", may be spelt
either with an <i>[a](https://en.wiktionary.org/wiki/-ant)</i> or with
an <i>[e](https://en.wiktionary.org/wiki/-ent)</i> in the final syllable
(see [descendant](https://en.wiktionary.org/wiki/descendant)). However,
the noun <i>descendant</i>, "one who is the progeny of someone", may be
spelt only with an <i>[a](https://en.wiktionary.org/wiki/-ant)</i>.
Compare also
<i>[dependent](https://en.wiktionary.org/wiki/dependent#English)</i> and
<i>[dependant](https://en.wiktionary.org/wiki/dependant#English)</i>.
2025-03-23 07:15:56 -07:00
Shunsuke Shibayama
ee51c2a389 [red-knot] fix ordering of ClassDef semantic index building (#16915)
## Summary

From #16861

This PR fixes the incorrect `ClassDef` handling of
`SemanticIndexBuilder::visit_stmt`, which fixes some of the incorrect
behavior of referencing the class itself in the class scope (a complete
fix requires a different fix, which will be done in the another PR).

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-03-23 13:23:12 +00:00
Vasco Schiavo
bb07ccd783 [pylint] Fix typo in documentation of PLC1802 (#16920) 2025-03-23 06:17:33 -05:00
John Stilley
c35f2bfe32 Fixing various spelling errors (#16924)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

This is a cleanup PR. I am fixing various English language spelling
errors. This is mostly in docs and docstrings.

## Test Plan

The usual CI tests were run. I tried to build the docs (though I had
some troubles there). The testing needs here are, I trust, very low
impact. (Though I would happily test more.)
2025-03-23 08:08:40 +00:00
Micha Reiser
7fb765d9b6 [red-knot] Log sys-prefix origin for easier debugging (#16921)
## Summary

Log the origin of the sys path prefix. This should help with debugging
if someone doesn't understand
why Red Knot picks up a certain venv.

## Test Plan

Ran the CLI and tested that it logs the origin
2025-03-23 08:06:04 +00:00
Dhruv Manilawala
0360c6b219 [red-knot] Support calling a typing.Callable (#16888)
## Summary

Part of #15382, this PR adds support for calling a variable that's
annotated with `typing.Callable`.

## Test Plan

Add test cases in a new `call/annotation.md` file.
2025-03-23 02:39:33 +05:30
Dhruv Manilawala
1cffb323bc [red-knot] Check assignability for two callable types (#16845)
## Summary

Part of #15382

This PR adds support for checking the assignability of two general
callable types.

This is built on top of #16804 by including the gradual parameters check
and accepting a function that performs the check between the two types.

## Test Plan

Update `is_assignable_to.md` with callable types section.
2025-03-23 02:28:44 +05:30
Matthew Mckee
92028efe3d [red-knot] Fix disambiguate display for union types (#16907)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

When callables are displayed in unions, like:
```py
from typing import Callable


def foo(x: Callable[[], int] | None):
    # red-knot: Revealed type is `() -> int | None` [revealed-type]
    reveal_type(x)
```

This leaves the type rather ambiguous, to fix this we can add
parenthesis to callable type in union

Fixes #16893

## Test Plan

Update callable annotations tests

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-03-22 13:08:51 +01:00
Matthew Mckee
7b86f54c4c [red-knot] Add line number to mdtest panic message about language tag mismatch (#16906)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

Fixes #16898 

## Test Plan

Update test for lang mismatch panic
2025-03-22 13:05:31 +01:00
Brent Westbrook
e4f5fe8cf7 [syntax-errors] Duplicate type parameter names (#16858)
Summary
--

Detects duplicate type parameter names in function definitions, class
definitions, and type alias statements.

I also boxed the `type_params` field on `StmtTypeAlias` to make it
easier to
`match` with functions and classes. (That's the reason for the red-knot
code
owner review requests, sorry!)

Test Plan
--

New `ruff_python_syntax_errors` unit tests.

Fixes #11119.
2025-03-21 15:06:22 -04:00
Brent Westbrook
2baaedda6c [syntax-errors] Start detecting compile-time syntax errors (#16106)
## Summary

This PR implements the "greeter" approach for checking the AST for
syntax errors emitted by the CPython compiler. It introduces two main
infrastructural changes to support all of the compile-time errors:
1. Adds a new `semantic_errors` module to the parser crate with public
`SemanticSyntaxChecker` and `SemanticSyntaxError` types
2. Embeds a `SemanticSyntaxChecker` in the `ruff_linter::Checker` for
checking these errors in ruff

As a proof of concept, it also implements detection of two syntax
errors:
1. A reimplementation of
[`late-future-import`](https://docs.astral.sh/ruff/rules/late-future-import/)
(`F404`)
2. Detection of rebound comprehension iteration variables
(https://github.com/astral-sh/ruff/issues/14395)

## Test plan
Existing F404 tests, new inline tests in the `ruff_python_parser` crate,
and a linter CLI test showing an example of the `Message` output.

I also tested in VS Code, where `preview = false` and turning off syntax
errors both disable the new errors:


![image](https://github.com/user-attachments/assets/cf453d95-04f7-484b-8440-cb812f29d45e)

And on the playground, where `preview = false` also disables the errors:


![image](https://github.com/user-attachments/assets/a97570c4-1efa-439f-9d99-a54487dd6064)


Fixes #14395

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-03-21 14:45:25 -04:00
Ash Berlin-Taylor
b1deab83d9 Update replacement paths for AIR302 (#16876)
I am one of the core developers of Airflow and working on the
"airflow.sdk"
package, and this updates the recommended replacments to the correct
user-facing imports.[^1]

cc @Lee-W @uranusjr 

[^1]:
33f0f1d639/task-sdk/src/airflow/sdk/__init__.py (L68-L93)

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

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

## Summary

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

## Test Plan

Hope and pray? 😉 

I'm sure there are some snapshot files I'm supposed to fix first.


<!-- How was it tested? -->
2025-03-21 18:46:56 +01:00
Alex Waygood
d21d639ee0 [red-knot] Avoid false-positive diagnostics on * import statements (#16899)
## Summary

This PR removes false-positive diagnostics for `*` imports. Currently we
always emit a diagnostic for these statements unless the module we're
importing from has a symbol named `"*"` in its symbol table for the
global scope. (And if we were doing everything correctly, no module ever
would have a symbol named `"*"` in its global scope!)

The fix here is sort-of hacky and won't be what we'll want to do
long-term. However, I think it's useful to do this as a first step
since:
- It significantly reduces false positives when running on code that
uses `*` imports
- It "resets" the tests to a cleaner state with many fewer TODOs, making
it easier to see what the hard work is that's still to be done.

## Test Plan

`cargo test -p red_knot_python_semantic`
2025-03-21 14:41:49 +00:00
Alex Waygood
14eb4cac88 [red-knot] Add failing tests for * imports (#16873)
## Summary

This PR adds a suite of tests for wildcard (`*`) imports. The tests
nearly all fail for now, and those that don't, ahem, pass for the wrong
reasons...

I've tried to add TODO comments in all instances for places where we are
currently inferring the incorrect thing, incorrectly emitting a
diagnostic, or emitting a diagnostic with a bad error message.

## Test Plan

`cargo test -p red_knot_python_semantic`
2025-03-21 14:17:15 +00:00
Douglas Creager
c03c28d199 [red-knot] Break up call binding into two phases (#16546)
This breaks up call binding into two phases:

- **_Matching parameters_** just looks at the names and kinds
(positional/keyword) of each formal and actual parameters, and matches
them up. Most of the current call binding errors happen during this
phase.

- Once we have matched up formal and actual parameters, we can **_infer
types_** of each actual parameter, and **_check_** that each one is
assignable to the corresponding formal parameter type.

As part of this, we add information to each formal parameter about
whether it is a type form or not. Once [PEP
747](https://peps.python.org/pep-0747/) is finalized, we can hook that
up to this internal type form representation. This replaces the
`ParameterExpectations` type, which did the same thing in a more ad hoc
way.

While we're here, we add a new fluent API for building `Parameter`s,
which makes our signature constructors a bit nicer to read. We also
eliminate a TODO where we were consuming types from the argument list
instead of the bound parameter list when evaluating our special-case
known functions.

Closes #15460

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-03-21 09:38:11 -04:00
143 changed files with 10139 additions and 2984 deletions

View File

@@ -49,7 +49,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build sdist"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
command: sdist
args: --out dist
@@ -59,7 +59,7 @@ jobs:
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload sdist"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-sdist
path: dist
@@ -79,12 +79,12 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
target: x86_64
args: --release --locked --out dist
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-macos-x86_64
path: dist
@@ -99,7 +99,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-macos-x86_64
path: |
@@ -121,7 +121,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - aarch64"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
target: aarch64
args: --release --locked --out dist
@@ -131,7 +131,7 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-aarch64-apple-darwin
path: dist
@@ -146,7 +146,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-aarch64-apple-darwin
path: |
@@ -177,7 +177,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
target: ${{ matrix.platform.target }}
args: --release --locked --out dist
@@ -192,7 +192,7 @@ jobs:
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-${{ matrix.platform.target }}
path: dist
@@ -203,7 +203,7 @@ jobs:
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-${{ matrix.platform.target }}
path: |
@@ -230,7 +230,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -242,7 +242,7 @@ jobs:
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-${{ matrix.target }}
path: dist
@@ -260,7 +260,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-${{ matrix.target }}
path: |
@@ -304,7 +304,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
target: ${{ matrix.platform.target }}
manylinux: auto
@@ -325,7 +325,7 @@ jobs:
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-${{ matrix.platform.target }}
path: dist
@@ -343,7 +343,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-${{ matrix.platform.target }}
path: |
@@ -370,7 +370,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
@@ -387,7 +387,7 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/${{ env.MODULE_NAME }} --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-${{ matrix.target }}
path: dist
@@ -405,7 +405,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-${{ matrix.target }}
path: |
@@ -435,7 +435,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
@@ -454,7 +454,7 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/${{ env.MODULE_NAME }} --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-${{ matrix.platform.target }}
path: dist
@@ -472,7 +472,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-${{ matrix.platform.target }}
path: |

View File

@@ -96,7 +96,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digests
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: digests-${{ env.PLATFORM_TUPLE }}
path: /tmp/digests/*
@@ -113,7 +113,7 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
path: /tmp/digests
pattern: digests-*
@@ -256,7 +256,7 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
path: /tmp/digests
pattern: digests-*

View File

@@ -188,7 +188,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: |
rustup component add clippy
@@ -208,17 +208,17 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -239,7 +239,7 @@ jobs:
env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings"
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ruff
path: target/debug/ruff
@@ -254,17 +254,17 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -283,11 +283,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -310,7 +310,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
@@ -339,7 +339,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -362,7 +362,7 @@ jobs:
with:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
env:
MSRV: ${{ steps.msrv.outputs.value }}
@@ -370,11 +370,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -394,7 +394,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
workspaces: "fuzz -> target"
- name: "Install Rust toolchain"
@@ -422,8 +422,8 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
- uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
name: Download Ruff binary to test
id: download-cached-binary
with:
@@ -456,7 +456,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -492,7 +492,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
name: Download comparison Ruff binary
id: ruff-target
with:
@@ -587,13 +587,13 @@ jobs:
run: |
echo ${{ github.event.number }} > pr-number
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
name: Upload PR Number
with:
name: pr-number
path: pr-number
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
name: Upload Results
with:
name: ecosystem-result
@@ -625,11 +625,11 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
with:
args: --out dist
- name: "Test wheel"
@@ -651,13 +651,13 @@ jobs:
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install pre-commit"
run: pip install pre-commit
- name: "Cache pre-commit"
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
@@ -685,7 +685,7 @@ jobs:
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
with:
python-version: "3.13"
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
@@ -694,7 +694,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -724,7 +724,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Run checks"
@@ -757,7 +757,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
name: Download development ruff binary
id: ruff-target
with:
@@ -793,7 +793,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: 22
@@ -824,13 +824,13 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
with:
tool: cargo-codspeed

View File

@@ -34,12 +34,12 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
- uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: Build ruff
# A debug build means the script runs slower once it gets started,
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI

View File

@@ -37,7 +37,7 @@ jobs:
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: Build Red Knot
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.

View File

@@ -35,9 +35,9 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
workspaces: "ruff"
- name: Install Rust toolchain
@@ -81,13 +81,13 @@ jobs:
echo ${{ github.event.number }} > pr-number
- name: Upload diff
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: mypy_primer_diff
path: mypy_primer.diff
- name: Upload pr-number
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pr-number
path: pr-number

View File

@@ -68,7 +68,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}

View File

@@ -8,10 +8,10 @@ on:
branches: [main]
paths:
- "crates/red_knot*/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"
- "playground"
- "crates/ruff_db/**"
- "crates/ruff_python_ast/**"
- "crates/ruff_python_parser/**"
- "playground/**"
- ".github/workflows/publish-knot-playground.yml"
concurrency:

View File

@@ -22,8 +22,8 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
pattern: wheels-*
path: wheels

View File

@@ -68,7 +68,7 @@ jobs:
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
@@ -84,7 +84,7 @@ jobs:
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
@@ -125,14 +125,14 @@ jobs:
with:
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
pattern: artifacts-*
path: target/distrib/
@@ -150,7 +150,7 @@ jobs:
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: artifacts-build-global
path: |
@@ -175,14 +175,14 @@ jobs:
with:
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
pattern: artifacts-*
path: target/distrib/
@@ -196,7 +196,7 @@ jobs:
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
@@ -251,7 +251,7 @@ jobs:
submodules: recursive
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
pattern: artifacts-*
path: artifacts

View File

@@ -1421,11 +1421,11 @@ The following rules have been stabilized and are no longer in preview:
The following behaviors have been stabilized:
- [`cancel-scope-no-checkpoint`](https://docs.astral.sh/ruff/rules/cancel-scope-no-checkpoint/) (`ASYNC100`): Support `asyncio` and `anyio` context mangers.
- [`async-function-with-timeout`](https://docs.astral.sh/ruff/rules/async-function-with-timeout/) (`ASYNC109`): Support `asyncio` and `anyio` context mangers.
- [`async-busy-wait`](https://docs.astral.sh/ruff/rules/async-busy-wait/) (`ASYNC110`): Support `asyncio` and `anyio` context mangers.
- [`async-zero-sleep`](https://docs.astral.sh/ruff/rules/async-zero-sleep/) (`ASYNC115`): Support `anyio` context mangers.
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever/) (`ASYNC116`): Support `anyio` context mangers.
- [`cancel-scope-no-checkpoint`](https://docs.astral.sh/ruff/rules/cancel-scope-no-checkpoint/) (`ASYNC100`): Support `asyncio` and `anyio` context managers.
- [`async-function-with-timeout`](https://docs.astral.sh/ruff/rules/async-function-with-timeout/) (`ASYNC109`): Support `asyncio` and `anyio` context managers.
- [`async-busy-wait`](https://docs.astral.sh/ruff/rules/async-busy-wait/) (`ASYNC110`): Support `asyncio` and `anyio` context managers.
- [`async-zero-sleep`](https://docs.astral.sh/ruff/rules/async-zero-sleep/) (`ASYNC115`): Support `anyio` context managers.
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever/) (`ASYNC116`): Support `anyio` context managers.
The following fixes have been stabilized:

63
Cargo.lock generated
View File

@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -487,7 +487,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -918,7 +918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1071,16 +1071,16 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
@@ -1499,7 +1499,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1659,9 +1659,9 @@ dependencies = [
[[package]]
name = "libmimalloc-sys"
version = "0.1.39"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502"
dependencies = [
"cc",
"libc",
@@ -1797,9 +1797,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mimalloc"
version = "0.1.43"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1"
dependencies = [
"libmimalloc-sys",
]
@@ -2384,6 +2384,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.8.5"
@@ -2441,7 +2447,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.1",
"getrandom 0.3.2",
]
[[package]]
@@ -2637,15 +2643,16 @@ version = "0.0.0"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.1",
"getrandom 0.3.2",
"js-sys",
"log",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
"ruff_notebook",
"ruff_python_ast",
"ruff_source_file",
"ruff_text_size",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
]
@@ -3289,7 +3296,7 @@ version = "0.11.2"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.1",
"getrandom 0.3.2",
"js-sys",
"log",
"ruff_formatter",
@@ -3379,7 +3386,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3392,7 +3399,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.3",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3769,15 +3776,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.19.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
dependencies = [
"fastrand",
"getrandom 0.3.1",
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.2",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4270,7 +4277,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.1",
"getrandom 0.3.2",
"js-sys",
"rand 0.9.0",
"uuid-macro-internal",
@@ -4378,9 +4385,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
@@ -4543,7 +4550,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -4796,9 +4803,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.0",
]

View File

@@ -12,7 +12,7 @@ use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform};
use ruff_db::files::{system_path_to_file, File, FileError};
use ruff_db::source::source_text;
use ruff_db::system::{
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
file_time_now, OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
};
use ruff_db::{Db as _, Upcast};
use ruff_python_ast::PythonVersion;
@@ -462,7 +462,7 @@ fn update_file(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()
std::thread::sleep(Duration::from_nanos(10));
filetime::set_file_handle_times(&file, None, Some(filetime::FileTime::now()))?;
filetime::set_file_handle_times(&file, None, Some(file_time_now()))?;
}
}

View File

@@ -64,7 +64,7 @@ impl ProjectMetadata {
}
/// Loads a project from a set of options with an optional pyproject-project table.
pub(crate) fn from_options(
pub fn from_options(
mut options: Options,
root: SystemPathBuf,
project: Option<&Project>,

View File

@@ -37,11 +37,19 @@ pub struct Options {
impl Options {
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, KnotTomlError> {
let _guard = ValueSourceGuard::new(source);
let _guard = ValueSourceGuard::new(source, true);
let options = toml::from_str(content)?;
Ok(options)
}
pub fn deserialize_with<'de, D>(source: ValueSource, deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let _guard = ValueSourceGuard::new(source, false);
Self::deserialize(deserializer)
}
pub(crate) fn to_program_settings(
&self,
project_root: &SystemPath,

View File

@@ -34,7 +34,7 @@ impl PyProject {
content: &str,
source: ValueSource,
) -> Result<Self, PyProjectError> {
let _guard = ValueSourceGuard::new(source);
let _guard = ValueSourceGuard::new(source, true);
toml::from_str(content).map_err(PyProjectError::TomlSyntax)
}
}

View File

@@ -19,6 +19,7 @@ pub enum ValueSource {
/// Ideally, we'd use [`ruff_db::files::File`] but we can't because the database hasn't been
/// created when loading the configuration.
File(Arc<SystemPathBuf>),
/// The value comes from a CLI argument, while it's left open if specified using a short argument,
/// long argument (`--extra-paths`) or `--config key=value`.
Cli,
@@ -41,18 +42,18 @@ thread_local! {
/// Use the [`ValueSourceGuard`] to initialize the thread local before calling into any
/// deserialization code. It ensures that the thread local variable gets cleaned up
/// once deserialization is done (once the guard gets dropped).
static VALUE_SOURCE: RefCell<Option<ValueSource>> = const { RefCell::new(None) };
static VALUE_SOURCE: RefCell<Option<(ValueSource, bool)>> = const { RefCell::new(None) };
}
/// Guard to safely change the [`VALUE_SOURCE`] for the current thread.
#[must_use]
pub(super) struct ValueSourceGuard {
prev_value: Option<ValueSource>,
prev_value: Option<(ValueSource, bool)>,
}
impl ValueSourceGuard {
pub(super) fn new(source: ValueSource) -> Self {
let prev = VALUE_SOURCE.replace(Some(source));
pub(super) fn new(source: ValueSource, is_toml: bool) -> Self {
let prev = VALUE_SOURCE.replace(Some((source, is_toml)));
Self { prev_value: prev }
}
}
@@ -265,18 +266,24 @@ where
where
D: Deserializer<'de>,
{
let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
let span = spanned.span();
let range = TextRange::new(
TextSize::try_from(span.start).expect("Configuration file to be smaller than 4GB"),
TextSize::try_from(span.end).expect("Configuration file to be smaller than 4GB"),
);
VALUE_SOURCE.with_borrow(|source| {
let (source, has_span) = source.clone().unwrap();
Ok(VALUE_SOURCE.with_borrow(|source| {
let source = source.clone().unwrap();
if has_span {
let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
let span = spanned.span();
let range = TextRange::new(
TextSize::try_from(span.start)
.expect("Configuration file to be smaller than 4GB"),
TextSize::try_from(span.end)
.expect("Configuration file to be smaller than 4GB"),
);
Self::with_range(spanned.into_inner(), source, range)
}))
Ok(Self::with_range(spanned.into_inner(), source, range))
} else {
Ok(Self::new(T::deserialize(deserializer)?, source))
}
})
}
}

View File

@@ -4,8 +4,10 @@ References:
- <https://typing.readthedocs.io/en/latest/spec/callables.html#callable>
TODO: Use `collections.abc` as importing from `typing` is deprecated but this requires support for
`*` imports. See: <https://docs.python.org/3/library/typing.html#deprecated-aliases>.
Note that `typing.Callable` is deprecated at runtime, in favour of `collections.abc.Callable` (see:
<https://docs.python.org/3/library/typing.html#deprecated-aliases>). However, removal of
`typing.Callable` is not currently planned, and the canonical location of the stub for the symbol in
typeshed is still `typing.pyi`.
## Invalid forms
@@ -152,6 +154,39 @@ def _(c: Callable[[int, str], int]):
reveal_type(c) # revealed: (int, str, /) -> int
```
## Union
```py
from typing import Callable, Union
def _(
c: Callable[[Union[int, str]], int] | None,
d: None | Callable[[Union[int, str]], int],
e: None | Callable[[Union[int, str]], int] | int,
):
reveal_type(c) # revealed: ((int | str, /) -> int) | None
reveal_type(d) # revealed: None | ((int | str, /) -> int)
reveal_type(e) # revealed: None | ((int | str, /) -> int) | int
```
## Intersection
```py
from typing import Callable, Union
from knot_extensions import Intersection, Not
def _(
c: Intersection[Callable[[Union[int, str]], int], int],
d: Intersection[int, Callable[[Union[int, str]], int]],
e: Intersection[int, Callable[[Union[int, str]], int], str],
f: Intersection[Not[Callable[[int, str], Intersection[int, str]]]],
):
reveal_type(c) # revealed: ((int | str, /) -> int) & int
reveal_type(d) # revealed: int & ((int | str, /) -> int)
reveal_type(e) # revealed: int & ((int | str, /) -> int) & str
reveal_type(f) # revealed: ~((int, str, /) -> int & str)
```
## Nested
A nested `Callable` as one of the parameter types:

View File

@@ -81,8 +81,7 @@ reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[SetSubclass], Literal[set], Unknown, Literal[object]]
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
@@ -114,8 +113,7 @@ reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# TODO: Should be (DequeSubclass, deque, MutableSequence, Sequence, Reversible, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Unknown, Literal[object]]
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...

View File

@@ -29,8 +29,6 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
# TODO: should understand the annotation
reveal_type(kwargs) # revealed: dict
# TODO: not an error; remove once `call` is implemented for `Callable`
# error: [call-non-callable]
return callback(42, *args, **kwargs)
class Foo:

View File

@@ -551,6 +551,7 @@ reveal_type(C().x) # revealed: str
class C:
def __init__(self) -> None:
# error: [too-many-positional-arguments]
# error: [invalid-argument-type]
self.x: int = len(1, 2, 3)
```
@@ -697,10 +698,10 @@ class Base:
self.defined_in_init: str | None = "value in base"
class Intermediate(Base):
# Re-declaring base class attributes with the *same *type is fine:
# Redeclaring base class attributes with the *same *type is fine:
base_class_attribute_1: str | None = None
# Re-declaring them with a *narrower type* is unsound, because modifications
# Redeclaring them with a *narrower type* is unsound, because modifications
# through a `Base` reference could violate that constraint.
#
# Mypy does not report an error here, but pyright does: "… overrides symbol
@@ -712,7 +713,7 @@ class Intermediate(Base):
# TODO: This should be an error
base_class_attribute_2: str
# Re-declaring attributes with a *wider type* directly violates LSP.
# Redeclaring attributes with a *wider type* directly violates LSP.
#
# In this case, both mypy and pyright report an error.
#

View File

@@ -0,0 +1,43 @@
# `typing.Callable`
```py
from typing import Callable
def _(c: Callable[[], int]):
reveal_type(c()) # revealed: int
def _(c: Callable[[int, str], int]):
reveal_type(c(1, "a")) # revealed: int
# error: [invalid-argument-type] "Object of type `Literal["a"]` cannot be assigned to parameter 1; expected type `int`"
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2; expected type `str`"
reveal_type(c("a", 1)) # revealed: int
```
The `Callable` annotation can only be used to describe positional-only parameters.
```py
def _(c: Callable[[int, str], None]):
# error: [unknown-argument] "Argument `a` does not match any known parameter"
# error: [unknown-argument] "Argument `b` does not match any known parameter"
# error: [missing-argument] "No arguments provided for required parameters 1, 2"
reveal_type(c(a=1, b="b")) # revealed: None
```
If the annotation uses a gradual form (`...`) for the parameter list, then it can accept any kind of
parameter with any type.
```py
def _(c: Callable[..., int]):
reveal_type(c()) # revealed: int
reveal_type(c(1)) # revealed: int
reveal_type(c(1, "str", False, a=[1, 2], b=(3, 4))) # revealed: int
```
An invalid `Callable` form can accept any parameters and will return `Unknown`.
```py
# error: [invalid-type-form]
def _(c: Callable[42, str]):
reveal_type(c()) # revealed: Unknown
```

View File

@@ -37,8 +37,6 @@ def foo() -> int:
return 42
def decorator(func) -> Callable[[], int]:
# TODO: no error
# error: [invalid-return-type]
return foo
@decorator

View File

@@ -161,3 +161,17 @@ def _(flag: bool):
reveal_type(repr("string")) # revealed: Literal["'string'"]
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
```
## Cannot use an argument as both a value and a type form
```py
from knot_extensions import is_fully_static
def _(flag: bool):
if flag:
f = repr
else:
f = is_fully_static
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
reveal_type(f(int)) # revealed: str | Literal[True]
```

View File

@@ -13,17 +13,16 @@ reveal_type(cast("str", True)) # revealed: str
reveal_type(cast(int | str, 1)) # revealed: int | str
reveal_type(cast(val="foo", typ=int)) # revealed: int
# error: [invalid-type-form]
reveal_type(cast(Literal, True)) # revealed: Unknown
# error: [invalid-type-form]
reveal_type(cast(1, True)) # revealed: Unknown
# TODO: These should be errors
# error: [missing-argument] "No argument provided for required parameter `val` of function `cast`"
cast(str)
# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
cast(str, b"ar", "foo")
# TODO: Either support keyword arguments properly,
# or give a comprehensible error message saying they're unsupported
cast(val="foo", typ=int) # error: [unresolved-reference] "Name `foo` used when not defined"
```

View File

@@ -24,7 +24,7 @@ try:
help()
except* OSError as e:
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
# (needs homogenous tuples + generics)
# (needs homogeneous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
```
@@ -35,7 +35,7 @@ try:
help()
except* (TypeError, AttributeError) as e:
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
# (needs homogenous tuples + generics)
# (needs homogeneous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
```

View File

@@ -76,7 +76,7 @@ Using a parameter with default value:
lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1]
```
Using a variadic paramter:
Using a variadic parameter:
```py
# TODO: should be `tuple[Unknown, ...]` (needs generics)
@@ -98,3 +98,22 @@ expression.
```py
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> Unknown) -> Unknown
```
## Assignment
This does not enumerate all combinations of parameter kinds as that should be covered by the
[subtype tests for callable types](./../type_properties/is_subtype_of.md#callable).
```py
from typing import Callable
a1: Callable[[], None] = lambda: None
a2: Callable[[int], None] = lambda x: None
a3: Callable[[int, int], None] = lambda x, y, z=1: None
a4: Callable[[int, int], None] = lambda *args: None
# error: [invalid-assignment]
a5: Callable[[], None] = lambda x: None
# error: [invalid-assignment]
a6: Callable[[int], None] = lambda: None
```

View File

@@ -229,6 +229,6 @@ reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
```py
class NoDunderLen: ...
# TODO: Emit a diagnostic
# error: [invalid-argument-type]
reveal_type(len(NoDunderLen())) # revealed: int
```

View File

@@ -57,12 +57,30 @@ def f() -> int:
### In Protocol
```py
from typing import Protocol
from typing import Protocol, TypeVar
class Bar(Protocol):
def f(self) -> int: ...
class Baz(Bar):
# error: [invalid-return-type]
def f(self) -> int: ...
T = TypeVar("T")
class Qux(Protocol[T]):
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
class Foo(Protocol):
def f[T](self, v: T) -> T: ...
t = (Protocol, int)
reveal_type(t[0]) # revealed: typing.Protocol
class Lorem(t[0]):
def f(self) -> int: ...
```
### In abstract method
@@ -72,12 +90,20 @@ from abc import ABC, abstractmethod
class Foo(ABC):
@abstractmethod
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
@abstractmethod
# error: [invalid-return-type]
def g[T](self, x: T) -> T: ...
class Bar[T](ABC):
@abstractmethod
def f(self) -> int: ...
@abstractmethod
def g[T](self, x: T) -> T: ...
# error: [invalid-return-type]
def f() -> int: ...
@abstractmethod # Semantically meaningless, accepted nevertheless
def g() -> int: ...
```
### In overload

View File

@@ -183,8 +183,9 @@ In a non-stub file, without stringified forward references, this raises a `NameE
```py
class Base[T]: ...
# TODO: error: [unresolved-reference]
# TODO: the unresolved-reference error is correct, the non-subscriptable is not
# error: [non-subscriptable]
# error: [unresolved-reference]
class Sub(Base[Sub]): ...
```

View File

@@ -0,0 +1,951 @@
# Wildcard (`*`) imports
See the [Python language reference for import statements].
## Basic functionality
### A simple `*` import
`a.py`:
```py
X: bool = True
```
`b.py`:
```py
from a import *
reveal_type(X) # revealed: bool
print(Y) # error: [unresolved-reference]
```
### Overriding existing definition
`a.py`:
```py
X: bool = True
```
`b.py`:
```py
X = 42
reveal_type(X) # revealed: Literal[42]
from a import *
reveal_type(X) # revealed: bool
```
### Overridden by later definition
`a.py`:
```py
X: bool = True
```
`b.py`:
```py
from a import *
reveal_type(X) # revealed: bool
X = False
reveal_type(X) # revealed: Literal[False]
```
### Reaching across many modules
`a.py`:
```py
X: bool = True
```
`b.py`:
```py
from a import *
```
`c.py`:
```py
from b import *
```
`d.py`:
```py
from c import *
reveal_type(X) # revealed: bool
```
### A wildcard import constitutes a re-export
`a.pyi`:
```pyi
X: bool = True
```
`b.pyi`:
```pyi
Y: bool = False
```
`c.pyi`:
```pyi
from a import *
from b import Y
```
`d.py`:
```py
# `X` is accessible because the `*` import in `c` re-exports it from `c`
from c import X
# but `Y` is not because the `from b import Y` import does *not* constitute a re-export
from c import Y # error: [unresolved-import]
```
### Global-scope symbols defined using walrus expressions
`a.py`:
```py
X = (Y := 3) + 4
```
`b.py`:
```py
from a import *
reveal_type(X) # revealed: Unknown | Literal[7]
reveal_type(Y) # revealed: Unknown | Literal[3]
```
### Global-scope symbols defined in many other ways
`a.py`:
```py
import typing
from collections import OrderedDict
from collections import OrderedDict as Foo
A, B = 1, (C := 2)
D: (E := 4) = (F := 5) # error: [invalid-type-form]
for G in [1]:
...
for (H := 4).whatever in [2]: # error: [unresolved-attribute]
...
class I: ...
def J(): ...
type K = int
with () as L: # error: [invalid-context-manager]
...
match 42:
case {"something": M}:
...
case [*N]:
...
case [O]:
...
case P | Q:
...
case object(foo=R):
...
case object(S):
...
case T:
...
```
`b.py`:
```py
from a import *
# fmt: off
print((
A,
B,
C,
D,
E,
F,
G, # TODO: could emit diagnostic about being possibly unbound
H,
I,
J,
K,
L,
M, # TODO: could emit diagnostic about being possibly unbound
N, # TODO: could emit diagnostic about being possibly unbound
O, # TODO: could emit diagnostic about being possibly unbound
P, # TODO: could emit diagnostic about being possibly unbound
Q, # TODO: could emit diagnostic about being possibly unbound
R, # TODO: could emit diagnostic about being possibly unbound
S, # TODO: could emit diagnostic about being possibly unbound
T, # TODO: could emit diagnostic about being possibly unbound
typing,
OrderedDict,
Foo,
))
```
### Definitions in function-like scopes are not global definitions
Except for some cases involving walrus expressions inside comprehension scopes.
`a.py`:
```py
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
[a for a in Iterable()]
{b for b in Iterable()}
{c: c for c in Iterable()}
(d for d in Iterable())
lambda e: (f := 42)
# Definitions created by walruses in a comprehension scope are unique;
# they "leak out" of the scope and are stored in the surrounding scope
[(g := h * 2) for h in Iterable()]
[i for j in Iterable() if (i := j - 10) > 0]
{(k := l * 2): (m := l * 3) for l in Iterable()}
list(((o := p * 2) for p in Iterable()))
# A walrus expression nested inside several scopes *still* leaks out
# to the global scope:
[[[[(q := r) for r in Iterable()]] for _ in range(42)] for _ in range(42)]
# A walrus inside a lambda inside a comprehension does not leak out
[(lambda s=s: (t := 42))() for s in Iterable()]
```
`b.py`:
```py
from a import *
# error: [unresolved-reference]
reveal_type(a) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(b) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(c) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(d) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(e) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(f) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(h) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(j) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(p) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(r) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(s) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(t) # revealed: Unknown
# TODO: these should all reveal `Unknown | int`.
# (We don't generally model elsewhere in red-knot that bindings from walruses
# "leak" from comprehension scopes into outer scopes, but we should.)
# See https://github.com/astral-sh/ruff/issues/16954
reveal_type(g) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(m) # revealed: Unknown
reveal_type(o) # revealed: Unknown
reveal_type(q) # revealed: Unknown
```
### An annotation without a value is a definition in a stub but not a `.py` file
`a.pyi`:
```pyi
X: bool
```
`b.py`:
```py
Y: bool
```
`c.py`:
```py
from a import *
from b import *
reveal_type(X) # revealed: bool
# error: [unresolved-reference]
reveal_type(Y) # revealed: Unknown
```
### Global-scope names starting with underscores
Global-scope names starting with underscores are not imported from a `*` import (unless the module
has `__all__` and they are included in `__all__`):
`a.py`:
```py
_private: bool = False
__protected: bool = False
__dunder__: bool = False
___thunder___: bool = False
Y: bool = True
```
`b.py`:
```py
from a import *
# error: [unresolved-reference]
reveal_type(_private) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(__protected) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(__dunder__) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(___thunder___) # revealed: Unknown
reveal_type(Y) # revealed: bool
```
### All public symbols are considered re-exported from `.py` files
For `.py` files, we should consider all public symbols in the global namespace exported by that
module when considering which symbols are made available by a `*` import. Here, `b.py` does not use
the explicit `from a import X as X` syntax to explicitly mark it as publicly re-exported, and `X` is
not included in `__all__`; whether it should be considered a "public name" in module `b` is
ambiguous. We could consider an opt-in rule to warn the user when they use `X` in `c.py` that it was
not included in `__all__` and was not marked as an explicit re-export.
`a.py`:
```py
X: bool = True
```
`b.py`:
```py
from a import X
```
`c.py`:
```py
from b import *
# TODO: we could consider an opt-in diagnostic (see prose commentary above)
reveal_type(X) # revealed: bool
```
### Only explicit re-exports are considered re-exported from `.pyi` files
For `.pyi` files, we should consider all imports private to the stub unless they are included in
`__all__` or use the explicit `from foo import X as X` syntax.
`a.pyi`:
```pyi
X: bool = True
Y: bool = True
```
`b.pyi`:
```pyi
from a import X, Y as Y
```
`c.py`:
```py
from b import *
# This error is correct, as `X` is not considered re-exported from module `b`:
#
# error: [unresolved-reference]
reveal_type(X) # revealed: Unknown
reveal_type(Y) # revealed: bool
```
### Symbols in statically known branches
```toml
[environment]
python-version = "3.11"
```
`a.py`:
```py
import sys
if sys.version_info >= (3, 11):
X: bool = True
else:
Y: bool = False
Z: int = 42
```
`b.py`:
```py
Z: bool = True
from a import *
reveal_type(X) # revealed: bool
# TODO: should emit error: [unresolved-reference]
reveal_type(Y) # revealed: Unknown
# TODO: The `*` import should not be considered a redefinition
# of the global variable in this module, as the symbol in
# the `a` module is in a branch that is statically known
# to be dead code given the `python-version` configuration.
# Thus this should reveal `Literal[True]`.
reveal_type(Z) # revealed: Unknown
```
### Relative `*` imports
Relative `*` imports are also supported by Python:
`a/__init__.py`:
```py
```
`a/foo.py`:
```py
X: bool = True
```
`a/bar.py`:
```py
from .foo import *
reveal_type(X) # revealed: bool
```
## Star imports with `__all__`
If a module `x` contains `__all__`, all symbols included in `x.__all__` are imported by
`from x import *` (but no other symbols are).
### Simple tuple `__all__`
`a.py`:
```py
__all__ = ("X", "_private", "__protected", "__dunder__", "___thunder___")
X: bool = True
_private: bool = True
__protected: bool = True
__dunder__: bool = True
___thunder___: bool = True
Y: bool = False
```
`b.py`:
```py
from a import *
reveal_type(X) # revealed: bool
# TODO none of these should error, should all reveal `bool`
# error: [unresolved-reference]
reveal_type(_private) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(__protected) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(__dunder__) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(___thunder___) # revealed: Unknown
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
reveal_type(Y) # revealed: bool
```
### Simple list `__all__`
`a.py`:
```py
__all__ = ["X"]
X: bool = True
Y: bool = False
```
`b.py`:
```py
from a import *
reveal_type(X) # revealed: bool
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
reveal_type(Y) # revealed: bool
```
### `__all__` with additions later on in the global scope
The [typing spec] lists certain modifications to `__all__` that must be understood by type checkers.
`a.py`:
```py
FOO: bool = True
__all__ = ["FOO"]
```
`b.py`:
```py
import a
from a import *
__all__ = ["A"]
__all__ += ["B"]
__all__.append("C")
__all__.extend(["D"])
__all__.extend(("E",))
__all__.extend(a.__all__)
A: bool = True
B: bool = True
C: bool = True
D: bool = True
E: bool = True
F: bool = False
```
`c.py`:
```py
from b import *
reveal_type(A) # revealed: bool
reveal_type(B) # revealed: bool
reveal_type(C) # revealed: bool
reveal_type(D) # revealed: bool
reveal_type(E) # revealed: bool
reveal_type(FOO) # revealed: bool
# TODO should error with [unresolved-reference] & reveal `Unknown`
reveal_type(F) # revealed: bool
```
### `__all__` with subtractions later on in the global scope
Whereas there are many ways of adding to `__all__` that type checkers must support, there is only
one way of subtracting from `__all__` that type checkers are required to support:
`a.py`:
```py
__all__ = ["A", "B"]
__all__.remove("A")
A: bool = True
B: bool = True
```
`b.py`:
```py
from a import *
reveal_type(A) # revealed: bool
# TODO should emit an [unresolved-reference] diagnostic & reveal `Unknown`
reveal_type(B) # revealed: bool
```
### Invalid `__all__`
If `a.__all__` contains a member that does not refer to a symbol with bindings in the global scope,
a wildcard import from module `a` will fail at runtime.
TODO: Should we:
1. Emit a diagnostic at the invalid definition of `__all__` (which will not fail at runtime)?
1. Emit a diagnostic at the star-import from the module with the invalid `__all__` (which _will_
fail at runtime)?
1. Emit a diagnostic on both?
`a.py`:
```py
__all__ = ["a", "b"]
a = 42
```
`b.py`:
```py
# TODO we should consider emitting a diagnostic here (see prose description above)
from a import * # fails with `AttributeError: module 'foo' has no attribute 'b'` at runtime
```
### Dynamic `__all__`
If `__all__` contains members that are dynamically computed, we should check that all members of
`__all__` are assignable to `str`. For the purposes of evaluating `*` imports, however, we should
treat the module as though it has no `__all__` at all: all global-scope members of the module should
be considered imported by the import statement. We should probably also emit a warning telling the
user that we cannot statically determine the elements of `__all__`.
`a.py`:
```py
def f() -> str:
return "f"
def g() -> int:
return 42
# TODO we should emit a warning here for the dynamically constructed `__all__` member.
__all__ = [f()]
```
`b.py`:
```py
from a import *
# At runtime, `f` is imported but `g` is not; to avoid false positives, however,
# we treat `a` as though it does not have `__all__` at all,
# which would imply that both symbols would be present.
reveal_type(f) # revealed: Literal[f]
reveal_type(g) # revealed: Literal[g]
```
### `__all__` conditionally defined in a statically known branch
```toml
[environment]
python-version = "3.11"
```
`a.py`:
```py
import sys
X: bool = True
if sys.version_info >= (3, 11):
__all__ = ["X", "Y"]
Y: bool = True
else:
__all__ = ("Z",)
Z: bool = True
```
`b.py`:
```py
from a import *
reveal_type(X) # revealed: bool
reveal_type(Y) # revealed: bool
# TODO: should error with [unresolved-reference]
reveal_type(Z) # revealed: Unknown
```
### `__all__` conditionally mutated in a statically known branch
```toml
[environment]
python-version = "3.11"
```
`a.py`:
```py
import sys
__all__ = ["X"]
X: bool = True
if sys.version_info >= (3, 11):
__all__.append("Y")
Y: bool = True
else:
__all__.append("Z")
Z: bool = True
```
`b.py`:
```py
from a import *
reveal_type(X) # revealed: bool
reveal_type(Y) # revealed: bool
# TODO should have an [unresolved-reference] diagnostic
reveal_type(Z) # revealed: Unknown
```
### Empty `__all__`
An empty `__all__` is valid, but a `*` import from a module with an empty `__all__` results in 0
bindings being added from the import:
`a.py`:
```py
X: bool = True
__all__ = ()
```
`b.py`:
```py
Y: bool = True
__all__ = []
```
`c.py`:
```py
from a import *
from b import *
# TODO: both of these should have [unresolved-reference] diagnostics and reveal `Unknown`
reveal_type(X) # revealed: bool
reveal_type(Y) # revealed: bool
```
### `__all__` in a stub file
If a name is included in `__all__` in a stub file, it is considered re-exported even if it was only
defined using an import without the explicit `from foo import X as X` syntax:
`a.py`:
```py
X: bool = True
Y: bool = True
```
`b.py`:
```py
from a import X, Y
__all__ = ["X"]
```
`c.py`:
```py
from b import *
reveal_type(X) # revealed: bool
# TODO this should have an [unresolved-reference] diagnostic and reveal `Unknown`
reveal_type(Y) # revealed: bool
```
## `global` statements in non-global scopes
A `global` statement in a nested function scope, combined with a definition in the same function
scope of the name that was declared `global`, can add a symbol to the global namespace.
`a.py`:
```py
def f():
global g, h
g: bool = True
f()
```
`b.py`:
```py
from a import *
reveal_type(f) # revealed: Literal[f]
# TODO: false positive, should be `bool` with no diagnostic
# error: [unresolved-reference]
reveal_type(g) # revealed: Unknown
# this diagnostic is accurate, though!
# error: [unresolved-reference]
reveal_type(h) # revealed: Unknown
```
## Cyclic star imports
Believe it or not, this code does _not_ raise an exception at runtime!
`a.py`:
```py
from b import *
A: bool = True
```
`b.py`:
```py
from a import *
B: bool = True
```
`c.py`:
```py
from a import *
reveal_type(A) # revealed: bool
reveal_type(B) # revealed: bool
```
## Integration test: `collections.abc`
The `collections.abc` standard-library module provides a good integration test, as all its symbols
are present due to `*` imports.
```py
import typing
import collections.abc
reveal_type(collections.abc.Sequence) # revealed: Literal[Sequence]
reveal_type(collections.abc.Callable) # revealed: typing.Callable
```
## Invalid `*` imports
### Unresolved module
If the module is unresolved, we emit a diagnostic just like for any other unresolved import:
```py
# TODO: not a great error message
from foo import * # error: [unresolved-import] "Cannot resolve import `foo`"
```
### Nested scope
A `*` import in a nested scope are always a syntax error. Red-knot does not infer any bindings from
them:
`a.py`:
```py
X: bool = True
```
`b.py`:
```py
def f():
# TODO: we should emit a syntax errror here (tracked by https://github.com/astral-sh/ruff/issues/11934)
from a import *
# error: [unresolved-reference]
reveal_type(X) # revealed: Unknown
```
### `*` combined with other aliases in the list
`a.py`:
```py
X: bool = True
_Y: bool = False
_Z: bool = True
```
`b.py`:
<!-- blacken-docs:off -->
```py
from a import *, _Y # error: [invalid-syntax]
# The import statement above is invalid syntax,
# but it's pretty obvious that the user wanted to do a `*` import,
# so we import all public names from `a` anyway, to minimize cascading errors
reveal_type(X) # revealed: bool
reveal_type(_Y) # revealed: bool
```
These tests are more to assert that we don't panic on these various kinds of invalid syntax than
anything else:
`c.py`:
```py
from a import *, _Y # error: [invalid-syntax]
from a import _Y, *, _Z # error: [invalid-syntax]
from a import *, _Y as fooo # error: [invalid-syntax]
from a import *, *, _Y # error: [invalid-syntax]
```
<!-- blacken-docs:on -->
[python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
[typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols

View File

@@ -1,7 +1,7 @@
# Eager scopes
Some scopes are executed eagerly: references to variables defined in enclosing scopes are resolved
_immediately_. This is in constrast to (for instance) function scopes, where those references are
_immediately_. This is in contrast to (for instance) function scopes, where those references are
resolved when the function is called.
## Function definitions

View File

@@ -12,10 +12,7 @@ reveal_type(__file__) # revealed: str | None
reveal_type(__loader__) # revealed: LoaderProtocol | None
reveal_type(__package__) # revealed: str | None
reveal_type(__doc__) # revealed: str | None
# TODO: Should be `ModuleSpec | None`
# (needs support for `*` imports)
reveal_type(__spec__) # revealed: Unknown | None
reveal_type(__spec__) # revealed: ModuleSpec | None
reveal_type(__path__) # revealed: @Todo(generics)

View File

@@ -17,7 +17,7 @@ reveal_type(Bar) # revealed: Literal[Bar]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Literal[object]]
```
## Access to attributes declarated in stubs
## Access to attributes declared in stubs
Unlike regular Python modules, stub files often omit the right-hand side in declarations, including
in class scope. However, from the perspective of the type checker, we have to treat them as bindings

View File

@@ -393,4 +393,87 @@ static_assert(is_assignable_to(Never, type[str]))
static_assert(is_assignable_to(Never, type[Any]))
```
## Callable
The examples provided below are only a subset of the possible cases and include the ones with
gradual types. The cases with fully static types and using different combinations of parameter kinds
are covered in the [subtyping tests](./is_subtype_of.md#callable).
### Return type
```py
from knot_extensions import CallableTypeFromFunction, Unknown, static_assert, is_assignable_to
from typing import Any, Callable
static_assert(is_assignable_to(Callable[[], Any], Callable[[], int]))
static_assert(is_assignable_to(Callable[[], int], Callable[[], Any]))
static_assert(is_assignable_to(Callable[[], int], Callable[[], float]))
static_assert(not is_assignable_to(Callable[[], float], Callable[[], int]))
```
The return types should be checked even if the parameter types uses gradual form (`...`).
```py
static_assert(is_assignable_to(Callable[..., int], Callable[..., float]))
static_assert(not is_assignable_to(Callable[..., float], Callable[..., int]))
```
And, if there is no return type, the return type is `Unknown`.
```py
static_assert(is_assignable_to(Callable[[], Unknown], Callable[[], int]))
static_assert(is_assignable_to(Callable[[], int], Callable[[], Unknown]))
```
### Parameter types
A `Callable` which uses the gradual form (`...`) for the parameter types is consistent with any
input signature.
```py
from knot_extensions import CallableTypeFromFunction, static_assert, is_assignable_to
from typing import Any, Callable
static_assert(is_assignable_to(Callable[[], None], Callable[..., None]))
static_assert(is_assignable_to(Callable[..., None], Callable[..., None]))
static_assert(is_assignable_to(Callable[[int, float, str], None], Callable[..., None]))
```
Even if it includes any other parameter kinds.
```py
def positional_only(a: int, b: int, /) -> None: ...
def positional_or_keyword(a: int, b: int) -> None: ...
def variadic(*args: int) -> None: ...
def keyword_only(*, a: int, b: int) -> None: ...
def keyword_variadic(**kwargs: int) -> None: ...
def mixed(a: int, /, b: int, *args: int, c: int, **kwargs: int) -> None: ...
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
```
And, even if the parameters are unannotated.
```py
def positional_only(a, b, /) -> None: ...
def positional_or_keyword(a, b) -> None: ...
def variadic(*args) -> None: ...
def keyword_only(*, a, b) -> None: ...
def keyword_variadic(**kwargs) -> None: ...
def mixed(a, /, b, *args, c, **kwargs) -> None: ...
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
```
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

View File

@@ -68,6 +68,10 @@ static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
## Callable
The examples provided below are only a subset of the possible cases and only include the ones with
gradual types. The cases with fully static types and using different combinations of parameter kinds
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
```py
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
from typing import Any, Callable
@@ -94,7 +98,7 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[]
And, similarly for parameters with no annotations.
```py
def f2(a, b) -> None:
def f2(a, b, /) -> None:
return
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
@@ -115,8 +119,8 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
```
But, a function with either `*args` or `**kwargs` is not gradual equivalent to a callable with `...`
as the parameter type.
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
callable with `...` as the parameter type.
```py
def variadic_args(*args):
@@ -129,4 +133,25 @@ static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_arg
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
```
Parameter names, default values, and it's kind should also be considered when checking for gradual
equivalence.
```py
def f1(a): ...
def f2(b): ...
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
def f3(a=1): ...
def f4(a=2): ...
def f5(a): ...
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f5]))
def f6(a, /): ...
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f6]))
```
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize

View File

@@ -494,12 +494,34 @@ Return types are covariant.
```py
from typing import Callable
from knot_extensions import is_subtype_of, static_assert
from knot_extensions import is_subtype_of, static_assert, TypeOf
static_assert(is_subtype_of(Callable[[], int], Callable[[], float]))
static_assert(not is_subtype_of(Callable[[], float], Callable[[], int]))
```
### Optional return type
```py
from typing import Callable
from knot_extensions import is_subtype_of, static_assert, TypeOf
flag: bool = True
def optional_return_type() -> int | None:
if flag:
return 1
return None
def required_return_type() -> int:
return 1
static_assert(not is_subtype_of(TypeOf[optional_return_type], TypeOf[required_return_type]))
# TypeOf[some_function] is a singleton function-literal type, not a general callable type
static_assert(not is_subtype_of(TypeOf[required_return_type], TypeOf[optional_return_type]))
static_assert(is_subtype_of(TypeOf[optional_return_type], Callable[[], int | None]))
```
### Parameter types
Parameter types are contravariant.
@@ -507,13 +529,20 @@ Parameter types are contravariant.
#### Positional-only
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
from typing import Callable
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf
def float_param(a: float, /) -> None: ...
def int_param(a: int, /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
static_assert(is_subtype_of(TypeOf[int_param], Callable[[int], None]))
static_assert(is_subtype_of(TypeOf[float_param], Callable[[float], None]))
static_assert(not is_subtype_of(Callable[[int], None], TypeOf[int_param]))
static_assert(not is_subtype_of(Callable[[float], None], TypeOf[float_param]))
```
Parameter name is not required to be the same for positional-only parameters at the same position:
@@ -533,6 +562,10 @@ def multi_param2(b: int, c: bool, a: str, /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
static_assert(is_subtype_of(TypeOf[multi_param1], Callable[[float, int, str], None]))
static_assert(not is_subtype_of(Callable[[float, int, str], None], TypeOf[multi_param1]))
```
#### Positional-only with default value
@@ -541,7 +574,8 @@ If the parameter has a default value, it's treated as optional. This means that
corresponding position in the supertype does not need to have a default value.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
from typing import Callable
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf
def float_with_default(a: float = 1, /) -> None: ...
def int_with_default(a: int = 1, /) -> None: ...
@@ -552,6 +586,13 @@ static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], Call
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_without_default]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[int_with_default]))
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[int], None]))
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[], None]))
static_assert(is_subtype_of(TypeOf[float_with_default], Callable[[float], None]))
static_assert(not is_subtype_of(Callable[[int], None], TypeOf[int_with_default]))
static_assert(not is_subtype_of(Callable[[float], None], TypeOf[float_with_default]))
```
As the parameter itself is optional, it can be omitted in the supertype:

View File

@@ -17,7 +17,7 @@ use ruff_db::parsed::ParsedModule;
/// ## Usage in salsa tracked structs
/// It's important that [`AstNodeRef`] fields in salsa tracked structs are tracked fields
/// (attributed with `#[tracked`]). It prevents that the tracked struct gets a new ID
/// everytime the AST changes, which in turn, invalidates the result of any query
/// every time the AST changes, which in turn, invalidates the result of any query
/// that takes said tracked struct as a query argument or returns the tracked struct as part of its result.
///
/// For example, marking the [`AstNodeRef`] as tracked on `Expression`

View File

@@ -1,10 +1,15 @@
use std::fmt;
use std::num::NonZeroU32;
use std::ops::Deref;
use compact_str::{CompactString, ToCompactString};
use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_stdlib::identifiers::is_identifier;
use crate::{db::Db, module_resolver::file_to_module};
/// A module name, e.g. `foo.bar`.
///
/// Always normalized to the absolute form (never a relative module name, i.e., never `.foo`).
@@ -206,6 +211,29 @@ impl ModuleName {
pub fn ancestors(&self) -> impl Iterator<Item = Self> {
std::iter::successors(Some(self.clone()), Self::parent)
}
pub(crate) fn from_import_statement<'db>(
db: &'db dyn Db,
importing_file: File,
node: &'db ast::StmtImportFrom,
) -> Result<Self, ModuleNameResolutionError> {
let ast::StmtImportFrom {
module,
level,
names: _,
range: _,
} = node;
let module = module.as_deref();
if let Some(level) = NonZeroU32::new(*level) {
relative_module_name(db, importing_file, module, level)
} else {
module
.and_then(Self::new)
.ok_or(ModuleNameResolutionError::InvalidSyntax)
}
}
}
impl Deref for ModuleName {
@@ -234,3 +262,58 @@ impl std::fmt::Display for ModuleName {
f.write_str(&self.0)
}
}
/// Given a `from .foo import bar` relative import, resolve the relative module
/// we're importing `bar` from into an absolute [`ModuleName`]
/// using the name of the module we're currently analyzing.
///
/// - `level` is the number of dots at the beginning of the relative module name:
/// - `from .foo.bar import baz` => `level == 1`
/// - `from ...foo.bar import baz` => `level == 3`
/// - `tail` is the relative module name stripped of all leading dots:
/// - `from .foo import bar` => `tail == "foo"`
/// - `from ..foo.bar import baz` => `tail == "foo.bar"`
fn relative_module_name(
db: &dyn Db,
importing_file: File,
tail: Option<&str>,
level: NonZeroU32,
) -> Result<ModuleName, ModuleNameResolutionError> {
let module = file_to_module(db, importing_file)
.ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
let mut level = level.get();
if module.kind().is_package() {
level = level.saturating_sub(1);
}
let mut module_name = module
.name()
.ancestors()
.nth(level as usize)
.ok_or(ModuleNameResolutionError::TooManyDots)?;
if let Some(tail) = tail {
let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
module_name.extend(&tail);
}
Ok(module_name)
}
/// Various ways in which resolving a [`ModuleName`]
/// from an [`ast::StmtImport`] or [`ast::StmtImportFrom`] node might fail
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) enum ModuleNameResolutionError {
/// The import statement has invalid syntax
InvalidSyntax,
/// We couldn't resolve the file we're currently analyzing back to a module
/// (Only necessary for relative import statements)
UnknownCurrentModule,
/// The relative import statement seems to take us outside of the module search path
/// (e.g. our current module is `foo.bar`, and the relative import statement in `foo.bar`
/// is `from ....baz import spam`)
TooManyDots,
}

View File

@@ -224,6 +224,9 @@ impl SearchPaths {
let site_packages_paths = match python_path {
PythonPath::SysPrefix(sys_prefix, origin) => {
tracing::debug!(
"Discovering site-packages paths from sys-prefix `{sys_prefix}` ({origin}')"
);
// TODO: We may want to warn here if the venv's python version is older
// than the one resolved in the program settings because it indicates
// that the `target-version` is incorrectly configured or that the

View File

@@ -14,7 +14,7 @@ use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIds;
use crate::semantic_index::attribute_assignment::AttributeAssignments;
use crate::semantic_index::builder::SemanticIndexBuilder;
use crate::semantic_index::definition::{Definition, DefinitionNodeKey};
use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolTable,
@@ -29,6 +29,7 @@ pub mod definition;
pub mod expression;
mod narrowing_constraints;
pub(crate) mod predicate;
mod re_exports;
pub mod symbol;
mod use_def;
mod visibility_constraints;
@@ -136,7 +137,7 @@ pub(crate) struct SemanticIndex<'db> {
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
/// Map from a node creating a definition to its definition.
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
/// Map from a standalone expression to its [`Expression`] ingredient.
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
@@ -235,8 +236,8 @@ impl<'db> SemanticIndex<'db> {
/// Returns an iterator over the descendent scopes of `scope`.
#[allow(unused)]
pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendentsIter {
DescendentsIter::new(self, scope)
pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter {
DescendantsIter::new(self, scope)
}
/// Returns an iterator over the direct child scopes of `scope`.
@@ -250,13 +251,37 @@ impl<'db> SemanticIndex<'db> {
AncestorsIter::new(self, scope)
}
/// Returns the [`Definition`] salsa ingredient for `definition_key`.
/// Returns the [`definition::Definition`] salsa ingredient(s) for `definition_key`.
///
/// There will only ever be >1 `Definition` associated with a `definition_key`
/// if the definition is created by a wildcard (`*`) import.
#[track_caller]
pub(crate) fn definition(
pub(crate) fn definitions(
&self,
definition_key: impl Into<DefinitionNodeKey>,
) -> &Definitions<'db> {
&self.definitions_by_node[&definition_key.into()]
}
/// Returns the [`definition::Definition`] salsa ingredient for `definition_key`.
///
/// ## Panics
///
/// If the number of definitions associated with the key is not exactly 1 and
/// the `debug_assertions` feature is enabled, this method will panic.
#[track_caller]
pub(crate) fn expect_single_definition(
&self,
definition_key: impl Into<DefinitionNodeKey> + std::fmt::Debug + Copy,
) -> Definition<'db> {
self.definitions_by_node[&definition_key.into()]
let definitions = self.definitions(definition_key);
debug_assert_eq!(
definitions.len(),
1,
"Expected exactly one definition to be associated with AST node {definition_key:?} but found {}",
definitions.len()
);
definitions[0]
}
/// Returns the [`Expression`] ingredient for an expression node.
@@ -280,7 +305,8 @@ impl<'db> SemanticIndex<'db> {
.copied()
}
/// Returns the id of the scope that `node` creates. This is different from [`Definition::scope`] which
/// Returns the id of the scope that `node` creates.
/// This is different from [`definition::Definition::scope`] which
/// returns the scope in which that definition is defined in.
#[track_caller]
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
@@ -339,55 +365,55 @@ impl<'a> Iterator for AncestorsIter<'a> {
impl FusedIterator for AncestorsIter<'_> {}
pub struct DescendentsIter<'a> {
pub struct DescendantsIter<'a> {
next_id: FileScopeId,
descendents: std::slice::Iter<'a, Scope>,
descendants: std::slice::Iter<'a, Scope>,
}
impl<'a> DescendentsIter<'a> {
impl<'a> DescendantsIter<'a> {
fn new(symbol_table: &'a SemanticIndex, scope_id: FileScopeId) -> Self {
let scope = &symbol_table.scopes[scope_id];
let scopes = &symbol_table.scopes[scope.descendents()];
let scopes = &symbol_table.scopes[scope.descendants()];
Self {
next_id: scope_id + 1,
descendents: scopes.iter(),
descendants: scopes.iter(),
}
}
}
impl<'a> Iterator for DescendentsIter<'a> {
impl<'a> Iterator for DescendantsIter<'a> {
type Item = (FileScopeId, &'a Scope);
fn next(&mut self) -> Option<Self::Item> {
let descendent = self.descendents.next()?;
let descendant = self.descendants.next()?;
let id = self.next_id;
self.next_id = self.next_id + 1;
Some((id, descendent))
Some((id, descendant))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.descendents.size_hint()
self.descendants.size_hint()
}
}
impl FusedIterator for DescendentsIter<'_> {}
impl FusedIterator for DescendantsIter<'_> {}
impl ExactSizeIterator for DescendentsIter<'_> {}
impl ExactSizeIterator for DescendantsIter<'_> {}
pub struct ChildrenIter<'a> {
parent: FileScopeId,
descendents: DescendentsIter<'a>,
descendants: DescendantsIter<'a>,
}
impl<'a> ChildrenIter<'a> {
fn new(module_symbol_table: &'a SemanticIndex, parent: FileScopeId) -> Self {
let descendents = DescendentsIter::new(module_symbol_table, parent);
let descendants = DescendantsIter::new(module_symbol_table, parent);
Self {
parent,
descendents,
descendants,
}
}
}
@@ -396,7 +422,7 @@ impl<'a> Iterator for ChildrenIter<'a> {
type Item = (FileScopeId, &'a Scope);
fn next(&mut self) -> Option<Self::Item> {
self.descendents
self.descendants
.find(|(_, scope)| scope.parent() == Some(self.parent))
}
}
@@ -1155,9 +1181,9 @@ def x():
let index = semantic_index(&db, file);
let descendents = index.descendent_scopes(FileScopeId::global());
let descendants = index.descendent_scopes(FileScopeId::global());
assert_eq!(
scope_names(descendents, &db, file),
scope_names(descendants, &db, file),
vec!["Test", "foo", "bar", "baz", "x"]
);

View File

@@ -12,19 +12,21 @@ use ruff_python_ast::{self as ast, ExprContext};
use crate::ast_node_ref::AstNodeRef;
use crate::module_name::ModuleName;
use crate::module_resolver::resolve_module;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
use crate::semantic_index::definition::{
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionCategory,
DefinitionNodeKey, DefinitionNodeRef, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef,
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef,
WithItemDefinitionNodeRef,
DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef,
ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, ImportFromDefinitionNodeRef,
MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef, WithItemDefinitionNodeRef,
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::predicate::{
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, ScopedPredicateId,
};
use crate::semantic_index::re_exports::exported_names;
use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId,
SymbolTableBuilder,
@@ -87,7 +89,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
imported_modules: FxHashSet<ModuleName>,
attribute_assignments: FxHashMap<FileScopeId, AttributeAssignments<'db>>,
@@ -147,6 +149,10 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_scope_info().file_scope_id
}
fn current_scope_is_global_scope(&self) -> bool {
self.scope_stack.len() == 1
}
/// Returns the scope ID of the surrounding class body scope if the current scope
/// is a method inside a class body. Returns `None` otherwise, e.g. if the current
/// scope is a function body outside of a class, or if the current scope is not a
@@ -229,7 +235,7 @@ impl<'db> SemanticIndexBuilder<'db> {
let children_end = self.scopes.next_index();
let popped_scope = &mut self.scopes[popped_scope_id];
popped_scope.extend_descendents(children_end);
popped_scope.extend_descendants(children_end);
if !popped_scope.is_eager() {
return popped_scope_id;
@@ -344,17 +350,55 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_symbol_table().mark_symbol_used(id);
}
fn add_entry_for_definition_key(&mut self, key: DefinitionNodeKey) -> &mut Definitions<'db> {
self.definitions_by_node.entry(key).or_default()
}
/// Add a [`Definition`] associated with the `definition_node` AST node.
///
/// ## Panics
///
/// This method panics if `debug_assertions` are enabled and the `definition_node` AST node
/// already has a [`Definition`] associated with it. This is an important invariant to maintain
/// for all nodes *except* [`ast::Alias`] nodes representing `*` imports.
fn add_definition(
&mut self,
symbol: ScopedSymbolId,
definition_node: impl Into<DefinitionNodeRef<'db>>,
definition_node: impl Into<DefinitionNodeRef<'db>> + std::fmt::Debug + Copy,
) -> Definition<'db> {
let (definition, num_definitions) =
self.push_additional_definition(symbol, definition_node);
debug_assert_eq!(
num_definitions,
1,
"Attempted to create multiple `Definition`s associated with AST node {definition_node:?}"
);
definition
}
/// Push a new [`Definition`] onto the list of definitions
/// associated with the `definition_node` AST node.
///
/// Returns a 2-element tuple, where the first element is the newly created [`Definition`]
/// and the second element is the number of definitions that are now associated with
/// `definition_node`.
///
/// This method should only be used when adding a definition associated with a `*` import.
/// All other nodes can only ever be associated with exactly 1 or 0 [`Definition`]s.
/// For any node other than an [`ast::Alias`] representing a `*` import,
/// prefer to use `self.add_definition()`, which ensures that this invariant is maintained.
fn push_additional_definition(
&mut self,
symbol: ScopedSymbolId,
definition_node: impl Into<DefinitionNodeRef<'db>>,
) -> (Definition<'db>, usize) {
let definition_node: DefinitionNodeRef<'_> = definition_node.into();
#[allow(unsafe_code)]
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
let category = kind.category(self.file.is_stub(self.db.upcast()));
let is_reexported = kind.is_reexported();
let definition = Definition::new(
self.db,
self.file,
@@ -365,10 +409,11 @@ impl<'db> SemanticIndexBuilder<'db> {
countme::Count::default(),
);
let existing_definition = self
.definitions_by_node
.insert(definition_node.key(), definition);
debug_assert_eq!(existing_definition, None);
let num_definitions = {
let definitions = self.add_entry_for_definition_key(definition_node.key());
definitions.push(definition);
definitions.len()
};
if category.is_binding() {
self.mark_symbol_bound(symbol);
@@ -390,7 +435,7 @@ impl<'db> SemanticIndexBuilder<'db> {
try_node_stack_manager.record_definition(self);
self.try_node_context_stack_manager = try_node_stack_manager;
definition
(definition, num_definitions)
}
fn record_expression_narrowing_constraint(
@@ -767,9 +812,10 @@ impl<'db> SemanticIndexBuilder<'db> {
// Insert a mapping from the inner Parameter node to the same definition. This
// ensures that calling `HasType::inferred_type` on the inner parameter returns
// a valid type (and doesn't panic)
let existing_definition = self
.definitions_by_node
.insert((&parameter.parameter).into(), definition);
let existing_definition = self.definitions_by_node.insert(
(&parameter.parameter).into(),
Definitions::single(definition),
);
debug_assert_eq!(existing_definition, None);
}
@@ -926,9 +972,6 @@ where
self.visit_decorator(decorator);
}
let symbol = self.add_symbol(class.name.id.clone());
self.add_definition(symbol, class);
self.with_type_params(
NodeWithScopeRef::ClassTypeParameters(class),
class.type_params.as_deref(),
@@ -943,6 +986,10 @@ where
builder.pop_scope()
},
);
// In Python runtime semantics, a class is registered after its scope is evaluated.
let symbol = self.add_symbol(class.name.id.clone());
self.add_definition(symbol, class);
}
ast::Stmt::TypeAlias(type_alias) => {
let symbol = self.add_symbol(
@@ -957,7 +1004,7 @@ where
self.with_type_params(
NodeWithScopeRef::TypeAliasTypeParameters(type_alias),
type_alias.type_params.as_ref(),
type_alias.type_params.as_deref(),
|builder| {
builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias));
builder.visit_expr(&type_alias.value);
@@ -990,7 +1037,54 @@ where
}
}
ast::Stmt::ImportFrom(node) => {
let mut found_star = false;
for (alias_index, alias) in node.names.iter().enumerate() {
if &alias.name == "*" {
// The following line maintains the invariant that every AST node that
// implements `Into<DefinitionNodeKey>` must have an entry in the
// `definitions_by_node` map. Maintaining this invariant ensures that
// `SemanticIndex::definitions` can always look up the definitions for a
// given AST node without panicking.
//
// The reason why maintaining this invariant requires special handling here
// is that some `Alias` nodes may be associated with 0 definitions:
// - If the import statement has invalid syntax: multiple `*` names in the `names` list
// (e.g. `from foo import *, bar, *`)
// - If the `*` import refers to a module that has 0 exported names.
// - If the module being imported from cannot be resolved.
self.add_entry_for_definition_key(alias.into());
if found_star {
continue;
}
found_star = true;
// Wildcard imports are invalid syntax everywhere except the top-level scope,
// and thus do not bind any definitions anywhere else
if !self.current_scope_is_global_scope() {
continue;
}
let Ok(module_name) =
ModuleName::from_import_statement(self.db, self.file, node)
else {
continue;
};
let Some(module) = resolve_module(self.db, &module_name) else {
continue;
};
for export in exported_names(self.db, module.file()) {
let symbol_id = self.add_symbol(export.clone());
let node_ref = StarImportDefinitionNodeRef { node, symbol_id };
self.push_additional_definition(symbol_id, node_ref);
}
continue;
}
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
(&asname.id, asname.id == alias.name.id)
} else {

View File

@@ -1,3 +1,5 @@
use std::ops::Deref;
use ruff_db::files::File;
use ruff_db::parsed::ParsedModule;
use ruff_python_ast as ast;
@@ -52,10 +54,42 @@ impl<'db> Definition<'db> {
}
}
/// One or more [`Definition`]s.
#[derive(Debug, Default, PartialEq, Eq, salsa::Update)]
pub struct Definitions<'db>(smallvec::SmallVec<[Definition<'db>; 1]>);
impl<'db> Definitions<'db> {
pub(crate) fn single(definition: Definition<'db>) -> Self {
Self(smallvec::smallvec![definition])
}
pub(crate) fn push(&mut self, definition: Definition<'db>) {
self.0.push(definition);
}
}
impl<'db> Deref for Definitions<'db> {
type Target = [Definition<'db>];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a, 'db> IntoIterator for &'a Definitions<'db> {
type Item = &'a Definition<'db>;
type IntoIter = std::slice::Iter<'a, Definition<'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum DefinitionNodeRef<'a> {
Import(ImportDefinitionNodeRef<'a>),
ImportFrom(ImportFromDefinitionNodeRef<'a>),
ImportStar(StarImportDefinitionNodeRef<'a>),
For(ForStmtDefinitionNodeRef<'a>),
Function(&'a ast::StmtFunctionDef),
Class(&'a ast::StmtClassDef),
@@ -178,12 +212,24 @@ impl<'a> From<MatchPatternDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
}
}
impl<'a> From<StarImportDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
fn from(node: StarImportDefinitionNodeRef<'a>) -> Self {
Self::ImportStar(node)
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct ImportDefinitionNodeRef<'a> {
pub(crate) alias: &'a ast::Alias,
pub(crate) is_reexported: bool,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct StarImportDefinitionNodeRef<'a> {
pub(crate) node: &'a ast::StmtImportFrom,
pub(crate) symbol_id: ScopedSymbolId,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct ImportFromDefinitionNodeRef<'a> {
pub(crate) node: &'a ast::StmtImportFrom,
@@ -253,6 +299,7 @@ impl<'db> DefinitionNodeRef<'db> {
alias: AstNodeRef::new(parsed, alias),
is_reexported,
}),
DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef {
node,
alias_index,
@@ -262,6 +309,13 @@ impl<'db> DefinitionNodeRef<'db> {
alias_index,
is_reexported,
}),
DefinitionNodeRef::ImportStar(star_import) => {
let StarImportDefinitionNodeRef { node, symbol_id } = star_import;
DefinitionKind::StarImport(StarImportDefinitionKind {
node: AstNodeRef::new(parsed, node),
symbol_id,
})
}
DefinitionNodeRef::Function(function) => {
DefinitionKind::Function(AstNodeRef::new(parsed, function))
}
@@ -376,6 +430,19 @@ impl<'db> DefinitionNodeRef<'db> {
alias_index,
is_reexported: _,
}) => (&node.names[alias_index]).into(),
// INVARIANT: for an invalid-syntax statement such as `from foo import *, bar, *`,
// we only create a `StarImportDefinitionKind` for the *first* `*` alias in the names list.
Self::ImportStar(StarImportDefinitionNodeRef { node, symbol_id: _ }) => node
.names
.iter()
.find(|alias| &alias.name == "*")
.expect(
"The `StmtImportFrom` node of a `StarImportDefinitionKind` instance \
should always have at least one `alias` with the name `*`.",
)
.into(),
Self::Function(node) => node.into(),
Self::Class(node) => node.into(),
Self::TypeAlias(node) => node.into(),
@@ -463,6 +530,7 @@ impl DefinitionCategory {
pub enum DefinitionKind<'db> {
Import(ImportDefinitionKind),
ImportFrom(ImportFromDefinitionKind),
StarImport(StarImportDefinitionKind),
Function(AstNodeRef<ast::StmtFunctionDef>),
Class(AstNodeRef<ast::StmtClassDef>),
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
@@ -492,6 +560,13 @@ impl DefinitionKind<'_> {
}
}
pub(crate) const fn as_star_import(&self) -> Option<&StarImportDefinitionKind> {
match self {
DefinitionKind::StarImport(import) => Some(import),
_ => None,
}
}
/// Returns the [`TextRange`] of the definition target.
///
/// A definition target would mainly be the node representing the symbol being defined i.e.,
@@ -502,6 +577,7 @@ impl DefinitionKind<'_> {
match self {
DefinitionKind::Import(import) => import.alias().range(),
DefinitionKind::ImportFrom(import) => import.alias().range(),
DefinitionKind::StarImport(import) => import.alias().range(),
DefinitionKind::Function(function) => function.name.range(),
DefinitionKind::Class(class) => class.name.range(),
DefinitionKind::TypeAlias(type_alias) => type_alias.name.range(),
@@ -531,6 +607,7 @@ impl DefinitionKind<'_> {
| DefinitionKind::TypeAlias(_)
| DefinitionKind::Import(_)
| DefinitionKind::ImportFrom(_)
| DefinitionKind::StarImport(_)
| DefinitionKind::TypeVar(_)
| DefinitionKind::ParamSpec(_)
| DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding,
@@ -589,7 +666,36 @@ impl<'db> From<Option<Unpack<'db>>> for TargetKind<'db> {
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub struct StarImportDefinitionKind {
node: AstNodeRef<ast::StmtImportFrom>,
symbol_id: ScopedSymbolId,
}
impl StarImportDefinitionKind {
pub(crate) fn import(&self) -> &ast::StmtImportFrom {
self.node.node()
}
pub(crate) fn alias(&self) -> &ast::Alias {
// INVARIANT: for an invalid-syntax statement such as `from foo import *, bar, *`,
// we only create a `StarImportDefinitionKind` for the *first* `*` alias in the names list.
self.node
.node()
.names
.iter()
.find(|alias| &alias.name == "*")
.expect(
"The `StmtImportFrom` node of a `StarImportDefinitionKind` instance \
should always have at least one `alias` with the name `*`.",
)
}
pub(crate) fn symbol_id(&self) -> ScopedSymbolId {
self.symbol_id
}
}
#[derive(Clone, Debug)]
pub struct MatchPatternDefinitionKind {
pattern: AstNodeRef<ast::Pattern>,
identifier: AstNodeRef<ast::Identifier>,

View File

@@ -0,0 +1,360 @@
//! A visitor and query to find all global-scope symbols that are exported from a module
//! when a wildcard import is used.
//!
//! For example, if a module `foo` contains `from bar import *`, which symbols from the global
//! scope of `bar` are imported into the global namespace of `foo`?
//!
//! ## Why is this a separate query rather than a part of semantic indexing?
//!
//! This query is called by the [`super::SemanticIndexBuilder`] in order to add the correct
//! [`super::Definition`]s to the semantic index of a module `foo` if `foo` has a
//! `from bar import *` statement in its global namespace. Adding the correct `Definition`s to
//! `foo`'s [`super::SemanticIndex`] requires knowing which symbols are exported from `bar`.
//!
//! If we determined the set of exported names during semantic indexing rather than as a
//! separate query, we would need to complete semantic indexing on `bar` in order to
//! complete analysis of the global namespace of `foo`. Since semantic indexing is somewhat
//! expensive, this would be undesirable. A separate query allows us to avoid this issue.
//!
//! An additional concern is that the recursive nature of this query means that it must be able
//! to handle cycles. We do this using fixpoint iteration; adding fixpoint iteration to the
//! whole [`super::semantic_index()`] query would probably be prohibitively expensive.
use ruff_db::{files::File, parsed::parsed_module};
use ruff_python_ast::{
self as ast,
name::Name,
visitor::{walk_expr, walk_pattern, walk_stmt, Visitor},
};
use rustc_hash::FxHashSet;
use crate::{module_name::ModuleName, resolve_module, Db};
fn exports_cycle_recover(
_db: &dyn Db,
_value: &FxHashSet<Name>,
_count: u32,
_file: File,
) -> salsa::CycleRecoveryAction<FxHashSet<Name>> {
salsa::CycleRecoveryAction::Iterate
}
fn exports_cycle_initial(_db: &dyn Db, _file: File) -> FxHashSet<Name> {
FxHashSet::default()
}
#[salsa::tracked(return_ref, cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial)]
pub(super) fn exported_names(db: &dyn Db, file: File) -> FxHashSet<Name> {
let module = parsed_module(db.upcast(), file);
let mut finder = ExportFinder::new(db, file);
finder.visit_body(module.suite());
finder.exports
}
struct ExportFinder<'db> {
db: &'db dyn Db,
file: File,
visiting_stub_file: bool,
exports: FxHashSet<Name>,
}
impl<'db> ExportFinder<'db> {
fn new(db: &'db dyn Db, file: File) -> Self {
Self {
db,
file,
visiting_stub_file: file.is_stub(db.upcast()),
exports: FxHashSet::default(),
}
}
fn possibly_add_export(&mut self, name: &Name) {
if name.starts_with('_') {
return;
}
self.exports.insert(name.clone());
}
}
impl<'db> Visitor<'db> for ExportFinder<'db> {
fn visit_alias(&mut self, alias: &'db ast::Alias) {
let ast::Alias { name, asname, .. } = alias;
if self.visiting_stub_file {
// If the source is a stub, names defined by imports are only exported
// if they use the explicit `foo as foo` syntax:
if asname.as_ref().is_some_and(|asname| asname.id == name.id) {
self.possibly_add_export(&name.id);
}
} else {
self.possibly_add_export(&asname.as_ref().unwrap_or(name).id);
}
}
fn visit_pattern(&mut self, pattern: &'db ast::Pattern) {
match pattern {
ast::Pattern::MatchAs(ast::PatternMatchAs {
pattern,
name,
range: _,
}) => {
if let Some(pattern) = pattern {
self.visit_pattern(pattern);
}
if let Some(name) = name {
// Wildcard patterns (`case _:`) do not bind names.
// Currently `self.possibly_add_export()` just ignores
// all names with leading underscores, but this will not always be the case
// (in the future we will want to support modules with `__all__ = ['_']`).
if name != "_" {
self.possibly_add_export(&name.id);
}
}
}
ast::Pattern::MatchMapping(ast::PatternMatchMapping {
patterns,
rest,
keys: _,
range: _,
}) => {
for pattern in patterns {
self.visit_pattern(pattern);
}
if let Some(rest) = rest {
self.possibly_add_export(&rest.id);
}
}
ast::Pattern::MatchStar(ast::PatternMatchStar { name, range: _ }) => {
if let Some(name) = name {
self.possibly_add_export(&name.id);
}
}
ast::Pattern::MatchSequence(_)
| ast::Pattern::MatchOr(_)
| ast::Pattern::MatchClass(_) => {
walk_pattern(self, pattern);
}
ast::Pattern::MatchSingleton(_) | ast::Pattern::MatchValue(_) => {}
}
}
fn visit_stmt(&mut self, stmt: &'db ruff_python_ast::Stmt) {
match stmt {
ast::Stmt::ClassDef(ast::StmtClassDef {
name,
decorator_list,
arguments,
type_params: _, // We don't want to visit the type params of the class
body: _, // We don't want to visit the body of the class
range: _,
}) => {
self.possibly_add_export(&name.id);
for decorator in decorator_list {
self.visit_decorator(decorator);
}
if let Some(arguments) = arguments {
self.visit_arguments(arguments);
}
}
ast::Stmt::FunctionDef(ast::StmtFunctionDef {
name,
decorator_list,
parameters,
returns,
type_params: _, // We don't want to visit the type params of the function
body: _, // We don't want to visit the body of the function
range: _,
is_async: _,
}) => {
self.possibly_add_export(&name.id);
for decorator in decorator_list {
self.visit_decorator(decorator);
}
self.visit_parameters(parameters);
if let Some(returns) = returns {
self.visit_expr(returns);
}
}
ast::Stmt::AnnAssign(ast::StmtAnnAssign {
target,
value,
annotation,
simple: _,
range: _,
}) => {
if value.is_some() || self.visiting_stub_file {
self.visit_expr(target);
}
self.visit_expr(annotation);
if let Some(value) = value {
self.visit_expr(value);
}
}
ast::Stmt::TypeAlias(ast::StmtTypeAlias {
name,
type_params: _,
value: _,
range: _,
}) => {
self.visit_expr(name);
// Neither walrus expressions nor statements cannot appear in type aliases;
// no need to recursively visit the `value` or `type_params`
}
ast::Stmt::ImportFrom(node) => {
let mut found_star = false;
for name in &node.names {
if &name.name.id == "*" {
if !found_star {
found_star = true;
self.exports.extend(
ModuleName::from_import_statement(self.db, self.file, node)
.ok()
.and_then(|module_name| resolve_module(self.db, &module_name))
.iter()
.flat_map(|module| exported_names(self.db, module.file()))
.cloned(),
);
}
} else {
self.visit_alias(name);
}
}
}
ast::Stmt::Import(_)
| ast::Stmt::AugAssign(_)
| ast::Stmt::While(_)
| ast::Stmt::If(_)
| ast::Stmt::With(_)
| ast::Stmt::Assert(_)
| ast::Stmt::Try(_)
| ast::Stmt::Expr(_)
| ast::Stmt::For(_)
| ast::Stmt::Assign(_)
| ast::Stmt::Match(_) => walk_stmt(self, stmt),
ast::Stmt::Global(_)
| ast::Stmt::Raise(_)
| ast::Stmt::Return(_)
| ast::Stmt::Break(_)
| ast::Stmt::Continue(_)
| ast::Stmt::IpyEscapeCommand(_)
| ast::Stmt::Delete(_)
| ast::Stmt::Nonlocal(_)
| ast::Stmt::Pass(_) => {}
}
}
fn visit_expr(&mut self, expr: &'db ast::Expr) {
match expr {
ast::Expr::Name(ast::ExprName { id, ctx, range: _ }) => {
if ctx.is_store() {
self.possibly_add_export(id);
}
}
ast::Expr::Lambda(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::EllipsisLiteral(_)
| ast::Expr::StringLiteral(_) => {}
// Walrus definitions "leak" from comprehension scopes into the comprehension's
// enclosing scope; they thus need special handling
ast::Expr::SetComp(_)
| ast::Expr::ListComp(_)
| ast::Expr::Generator(_)
| ast::Expr::DictComp(_) => {
let mut walrus_finder = WalrusFinder {
export_finder: self,
};
walk_expr(&mut walrus_finder, expr);
}
ast::Expr::BoolOp(_)
| ast::Expr::Named(_)
| ast::Expr::BinOp(_)
| ast::Expr::UnaryOp(_)
| ast::Expr::If(_)
| ast::Expr::Attribute(_)
| ast::Expr::Subscript(_)
| ast::Expr::Starred(_)
| ast::Expr::Call(_)
| ast::Expr::Compare(_)
| ast::Expr::Yield(_)
| ast::Expr::YieldFrom(_)
| ast::Expr::FString(_)
| ast::Expr::Tuple(_)
| ast::Expr::List(_)
| ast::Expr::Slice(_)
| ast::Expr::IpyEscapeCommand(_)
| ast::Expr::Dict(_)
| ast::Expr::Set(_)
| ast::Expr::Await(_) => walk_expr(self, expr),
}
}
}
struct WalrusFinder<'a, 'db> {
export_finder: &'a mut ExportFinder<'db>,
}
impl<'db> Visitor<'db> for WalrusFinder<'_, 'db> {
fn visit_expr(&mut self, expr: &'db ast::Expr) {
match expr {
// It's important for us to short-circuit here for lambdas specifically,
// as walruses cannot leak out of the body of a lambda function.
ast::Expr::Lambda(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::EllipsisLiteral(_)
| ast::Expr::StringLiteral(_)
| ast::Expr::Name(_) => {}
ast::Expr::Named(ast::ExprNamed {
target,
value: _,
range: _,
}) => {
if let ast::Expr::Name(ast::ExprName {
id,
ctx: ast::ExprContext::Store,
range: _,
}) = &**target
{
self.export_finder.possibly_add_export(id);
}
}
// We must recurse inside nested comprehensions,
// as even a walrus inside a comprehension inside a comprehension in the global scope
// will leak out into the global scope
ast::Expr::DictComp(_)
| ast::Expr::SetComp(_)
| ast::Expr::ListComp(_)
| ast::Expr::Generator(_)
| ast::Expr::BoolOp(_)
| ast::Expr::BinOp(_)
| ast::Expr::UnaryOp(_)
| ast::Expr::If(_)
| ast::Expr::Attribute(_)
| ast::Expr::Subscript(_)
| ast::Expr::Starred(_)
| ast::Expr::Call(_)
| ast::Expr::Compare(_)
| ast::Expr::Yield(_)
| ast::Expr::YieldFrom(_)
| ast::Expr::FString(_)
| ast::Expr::Tuple(_)
| ast::Expr::List(_)
| ast::Expr::Slice(_)
| ast::Expr::IpyEscapeCommand(_)
| ast::Expr::Dict(_)
| ast::Expr::Set(_)
| ast::Expr::Await(_) => walk_expr(self, expr),
}
}
}

View File

@@ -171,19 +171,19 @@ impl FileScopeId {
pub struct Scope {
parent: Option<FileScopeId>,
node: NodeWithScopeKind,
descendents: Range<FileScopeId>,
descendants: Range<FileScopeId>,
}
impl Scope {
pub(super) fn new(
parent: Option<FileScopeId>,
node: NodeWithScopeKind,
descendents: Range<FileScopeId>,
descendants: Range<FileScopeId>,
) -> Self {
Scope {
parent,
node,
descendents,
descendants,
}
}
@@ -199,12 +199,12 @@ impl Scope {
self.node().scope_kind()
}
pub fn descendents(&self) -> Range<FileScopeId> {
self.descendents.clone()
pub fn descendants(&self) -> Range<FileScopeId> {
self.descendants.clone()
}
pub(super) fn extend_descendents(&mut self, children_end: FileScopeId) {
self.descendents = self.descendents.start..children_end;
pub(super) fn extend_descendants(&mut self, children_end: FileScopeId) {
self.descendants = self.descendants.start..children_end;
}
pub(crate) fn is_eager(&self) -> bool {

View File

@@ -149,7 +149,7 @@ macro_rules! impl_binding_has_ty {
#[inline]
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
let index = semantic_index(model.db, model.file);
let binding = index.definition(self);
let binding = index.expect_single_definition(self);
binding_type(model.db, binding)
}
}
@@ -158,10 +158,19 @@ macro_rules! impl_binding_has_ty {
impl_binding_has_ty!(ast::StmtFunctionDef);
impl_binding_has_ty!(ast::StmtClassDef);
impl_binding_has_ty!(ast::Alias);
impl_binding_has_ty!(ast::Parameter);
impl_binding_has_ty!(ast::ParameterWithDefault);
impl HasType for ast::Alias {
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
if &self.name == "*" {
return Type::Never;
}
let index = semantic_index(model.db, model.file);
binding_type(model.db, index.expect_single_definition(self))
}
}
#[cfg(test)]
mod tests {
use ruff_db::files::system_path_to_file;

File diff suppressed because it is too large Load Diff

View File

@@ -110,25 +110,14 @@ impl<'db> UnionBuilder<'db> {
return self.collapse_to_object();
}
}
match to_remove[..] {
[] => self.elements.push(to_add),
[index] => self.elements[index] = to_add,
_ => {
let mut current_index = 0;
let mut to_remove = to_remove.into_iter();
let mut next_to_remove_index = to_remove.next();
self.elements.retain(|_| {
let retain = if Some(current_index) == next_to_remove_index {
next_to_remove_index = to_remove.next();
false
} else {
true
};
current_index += 1;
retain
});
self.elements.push(to_add);
if let Some((&first, rest)) = to_remove.split_first() {
self.elements[first] = to_add;
// We iterate in descending order to keep remaining indices valid after `swap_remove`.
for &index in rest.iter().rev() {
self.elements.swap_remove(index);
}
} else {
self.elements.push(to_add);
}
}
}

View File

@@ -4,14 +4,14 @@ use crate::Db;
mod arguments;
mod bind;
pub(super) use arguments::{Argument, CallArguments};
pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments};
pub(super) use bind::Bindings;
/// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was
/// unsuccessful.
///
/// The bindings are boxed so that we do not pass around large `Err` variants on the stack.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub(crate) struct CallError<'db>(pub(crate) CallErrorKind, pub(crate) Box<Bindings<'db>>);
/// The reason why calling a type failed.
@@ -32,7 +32,7 @@ pub(crate) enum CallErrorKind {
PossiblyNotCallable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub(super) enum CallDunderError<'db> {
/// The dunder attribute exists but it can't be called with the given arguments.
///

View File

@@ -1,88 +1,128 @@
use std::collections::VecDeque;
use std::ops::{Deref, DerefMut};
use super::Type;
/// Typed arguments for a single call, in source order.
/// Arguments for a single call, in source order.
#[derive(Clone, Debug, Default)]
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
pub(crate) struct CallArguments<'a>(VecDeque<Argument<'a>>);
impl<'a, 'db> CallArguments<'a, 'db> {
/// Create a [`CallArguments`] with no arguments.
pub(crate) fn none() -> Self {
Self(Vec::new())
impl<'a> CallArguments<'a> {
/// Invoke a function with an optional extra synthetic argument (for a `self` or `cls`
/// parameter) prepended to the front of this argument list. (If `bound_self` is none, the
/// function is invoked with the unmodified argument list.)
pub(crate) fn with_self<F, R>(&mut self, bound_self: Option<Type<'_>>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
if bound_self.is_some() {
self.0.push_front(Argument::Synthetic);
}
let result = f(self);
if bound_self.is_some() {
self.0.pop_front();
}
result
}
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
positional_tys
.into_iter()
.map(Argument::Positional)
.collect()
pub(crate) fn len(&self) -> usize {
self.0.len()
}
/// Prepend an extra positional argument.
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
let mut arguments = Vec::with_capacity(self.0.len() + 1);
arguments.push(Argument::Synthetic(self_ty));
arguments.extend_from_slice(&self.0);
Self(arguments)
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
self.0.iter()
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
self.0.first().map(Argument::ty)
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
self.0.get(1).map(Argument::ty)
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn third_argument(&self) -> Option<Type<'db>> {
self.0.get(2).map(Argument::ty)
pub(crate) fn iter(&self) -> impl Iterator<Item = Argument<'a>> + '_ {
self.0.iter().copied()
}
}
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
type Item = &'b Argument<'a, 'db>;
type IntoIter = std::slice::Iter<'b, Argument<'a, 'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'a, 'db> FromIterator<Argument<'a, 'db>> for CallArguments<'a, 'db> {
fn from_iter<T: IntoIterator<Item = Argument<'a, 'db>>>(iter: T) -> Self {
impl<'a> FromIterator<Argument<'a>> for CallArguments<'a> {
fn from_iter<T: IntoIterator<Item = Argument<'a>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Clone, Debug)]
pub(crate) enum Argument<'a, 'db> {
#[derive(Clone, Copy, Debug)]
pub(crate) enum Argument<'a> {
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
Synthetic(Type<'db>),
Synthetic,
/// A positional argument.
Positional(Type<'db>),
Positional,
/// A starred positional argument (e.g. `*args`).
Variadic(Type<'db>),
Variadic,
/// A keyword argument (e.g. `a=1`).
Keyword { name: &'a str, ty: Type<'db> },
Keyword(&'a str),
/// The double-starred keywords argument (e.g. `**kwargs`).
Keywords(Type<'db>),
Keywords,
}
impl<'db> Argument<'_, 'db> {
fn ty(&self) -> Type<'db> {
match self {
Self::Synthetic(ty) => *ty,
Self::Positional(ty) => *ty,
Self::Variadic(ty) => *ty,
Self::Keyword { name: _, ty } => *ty,
Self::Keywords(ty) => *ty,
/// Arguments for a single call, in source order, along with inferred types for each argument.
pub(crate) struct CallArgumentTypes<'a, 'db> {
arguments: CallArguments<'a>,
types: VecDeque<Type<'db>>,
}
impl<'a, 'db> CallArgumentTypes<'a, 'db> {
/// Create a [`CallArgumentTypes`] with no arguments.
pub(crate) fn none() -> Self {
let arguments = CallArguments::default();
let types = VecDeque::default();
Self { arguments, types }
}
/// Create a [`CallArgumentTypes`] from an iterator over non-variadic positional argument
/// types.
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
let types: VecDeque<_> = positional_tys.into_iter().collect();
let arguments = CallArguments(vec![Argument::Positional; types.len()].into());
Self { arguments, types }
}
/// Create a new [`CallArgumentTypes`] to store the inferred types of the arguments in a
/// [`CallArguments`]. Uses the provided callback to infer each argument type.
pub(crate) fn new<F>(arguments: CallArguments<'a>, mut f: F) -> Self
where
F: FnMut(usize, Argument<'a>) -> Type<'db>,
{
let types = arguments
.iter()
.enumerate()
.map(|(idx, argument)| f(idx, argument))
.collect();
Self { arguments, types }
}
/// Invoke a function with an optional extra synthetic argument (for a `self` or `cls`
/// parameter) prepended to the front of this argument list. (If `bound_self` is none, the
/// function is invoked with the unmodified argument list.)
pub(crate) fn with_self<F, R>(&mut self, bound_self: Option<Type<'db>>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
if let Some(bound_self) = bound_self {
self.arguments.0.push_front(Argument::Synthetic);
self.types.push_front(bound_self);
}
let result = f(self);
if bound_self.is_some() {
self.arguments.0.pop_front();
self.types.pop_front();
}
result
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
self.arguments.iter().zip(self.types.iter().copied())
}
}
impl<'a> Deref for CallArgumentTypes<'a, '_> {
type Target = CallArguments<'a>;
fn deref(&self) -> &CallArguments<'a> {
&self.arguments
}
}
impl<'a> DerefMut for CallArgumentTypes<'a, '_> {
fn deref_mut(&mut self) -> &mut CallArguments<'a> {
&mut self.arguments
}
}

View File

@@ -3,21 +3,24 @@
//! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a
//! union of types, each of which might contain multiple overloads.
use std::borrow::Cow;
use smallvec::SmallVec;
use super::{
Argument, CallArguments, CallError, CallErrorKind, CallableSignature, InferContext, Signature,
Signatures, Type,
Argument, CallArgumentTypes, CallArguments, CallError, CallErrorKind, CallableSignature,
InferContext, Signature, Signatures, Type,
};
use crate::db::Db;
use crate::symbol::{Boundness, Symbol};
use crate::types::diagnostic::{
CALL_NON_CALLABLE, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD,
PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT,
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS,
UNKNOWN_ARGUMENT,
};
use crate::types::signatures::{Parameter, ParameterForm};
use crate::types::{
todo_type, BoundMethodType, CallableType, ClassLiteralType, KnownClass, KnownFunction,
KnownInstanceType, UnionType,
};
use crate::types::signatures::Parameter;
use crate::types::{CallableType, UnionType};
use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
@@ -26,29 +29,75 @@ use ruff_text_size::Ranged;
/// compatible with _all_ of the types in the union for the call to be valid.
///
/// It's guaranteed that the wrapped bindings have no errors.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub(crate) struct Bindings<'db> {
pub(crate) callable_type: Type<'db>,
signatures: Signatures<'db>,
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a non-union
/// type.
elements: SmallVec<[CallableBinding<'db>; 1]>,
/// Whether each argument will be used as a value and/or a type form in this call.
pub(crate) argument_forms: Box<[Option<ParameterForm>]>,
conflicting_forms: Box<[bool]>,
}
impl<'db> Bindings<'db> {
/// Binds the arguments of a call site against a signature.
/// Match the arguments of a call site against the parameters of a collection of possibly
/// unioned, possibly overloaded signatures.
///
/// The returned bindings provide the return type of the call, the bound types for all
/// The returned bindings tell you which parameter (in each signature) each argument was
/// matched against. You can then perform type inference on each argument with extra context
/// about the expected parameter types. (You do this by creating a [`CallArgumentTypes`] object
/// from the `arguments` that you match against.)
///
/// Once you have argument types available, you can call [`check_types`][Self::check_types] to
/// verify that each argument type is assignable to the corresponding parameter type.
pub(crate) fn match_parameters(
signatures: Signatures<'db>,
arguments: &mut CallArguments<'_>,
) -> Self {
let mut argument_forms = vec![None; arguments.len()];
let mut conflicting_forms = vec![false; arguments.len()];
let elements: SmallVec<[CallableBinding<'db>; 1]> = signatures
.iter()
.map(|signature| {
CallableBinding::match_parameters(
signature,
arguments,
&mut argument_forms,
&mut conflicting_forms,
)
})
.collect();
Bindings {
signatures,
elements,
argument_forms: argument_forms.into(),
conflicting_forms: conflicting_forms.into(),
}
}
/// Verify that the type of each argument is assignable to type of the parameter that it was
/// matched to.
///
/// You must provide an `argument_types` that was created from the same `arguments` that you
/// provided to [`match_parameters`][Self::match_parameters].
///
/// We update the bindings to include the return type of the call, the bound types for all
/// parameters, and any errors resulting from binding the call, all for each union element and
/// overload (if any).
pub(crate) fn bind(
pub(crate) fn check_types(
mut self,
db: &'db dyn Db,
signatures: &Signatures<'db>,
arguments: &CallArguments<'_, 'db>,
argument_types: &mut CallArgumentTypes<'_, 'db>,
) -> Result<Self, CallError<'db>> {
let elements: SmallVec<[CallableBinding<'db>; 1]> = signatures
.into_iter()
.map(|signature| CallableBinding::bind(db, signature, arguments))
.collect();
for (signature, element) in self.signatures.iter().zip(&mut self.elements) {
element.check_types(db, signature, argument_types);
}
self.evaluate_known_cases(db);
// In order of precedence:
//
@@ -68,28 +117,28 @@ impl<'db> Bindings<'db> {
let mut all_ok = true;
let mut any_binding_error = false;
let mut all_not_callable = true;
for binding in &elements {
if self.conflicting_forms.contains(&true) {
all_ok = false;
any_binding_error = true;
all_not_callable = false;
}
for binding in &self.elements {
let result = binding.as_result();
all_ok &= result.is_ok();
any_binding_error |= matches!(result, Err(CallErrorKind::BindingError));
all_not_callable &= matches!(result, Err(CallErrorKind::NotCallable));
}
let bindings = Bindings {
callable_type: signatures.callable_type,
elements,
};
if all_ok {
Ok(bindings)
Ok(self)
} else if any_binding_error {
Err(CallError(CallErrorKind::BindingError, Box::new(bindings)))
Err(CallError(CallErrorKind::BindingError, Box::new(self)))
} else if all_not_callable {
Err(CallError(CallErrorKind::NotCallable, Box::new(bindings)))
Err(CallError(CallErrorKind::NotCallable, Box::new(self)))
} else {
Err(CallError(
CallErrorKind::PossiblyNotCallable,
Box::new(bindings),
Box::new(self),
))
}
}
@@ -98,6 +147,10 @@ impl<'db> Bindings<'db> {
self.elements.len() == 1
}
pub(crate) fn callable_type(&self) -> Type<'db> {
self.signatures.callable_type
}
/// Returns the return type of the call. For successful calls, this is the actual return type.
/// For calls with binding errors, this is a type that best approximates the return type. For
/// types that are not callable, returns `Type::Unknown`.
@@ -122,12 +175,22 @@ impl<'db> Bindings<'db> {
node,
format_args!(
"Object of type `{}` is not callable",
self.callable_type.display(context.db())
self.callable_type().display(context.db())
),
);
return;
}
for (index, conflicting_form) in self.conflicting_forms.iter().enumerate() {
if *conflicting_form {
context.report_lint(
&CONFLICTING_ARGUMENT_FORMS,
BindingError::get_node(node, Some(index)),
format_args!("Argument is used as both a value and a type form in call"),
);
}
}
// TODO: We currently only report errors for the first union element. Ideally, we'd report
// an error saying that the union type can't be called, followed by subdiagnostics
// explaining why.
@@ -135,6 +198,286 @@ impl<'db> Bindings<'db> {
first.report_diagnostics(context, node);
}
}
/// Evaluates the return type of certain known callables, where we have special-case logic to
/// determine the return type in a way that isn't directly expressible in the type system.
fn evaluate_known_cases(&mut self, db: &'db dyn Db) {
// Each special case listed here should have a corresponding clause in `Type::signatures`.
for binding in &mut self.elements {
let binding_type = binding.callable_type;
let Some((overload_index, overload)) = binding.matching_overload_mut() else {
continue;
};
match binding_type {
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
if function.has_known_class_decorator(db, KnownClass::Classmethod)
&& function.decorators(db).len() == 1
{
match overload.parameter_types() {
[_, Some(owner)] => {
overload.set_return_type(Type::Callable(
CallableType::BoundMethod(BoundMethodType::new(
db, function, *owner,
)),
));
}
[Some(instance), None] => {
overload.set_return_type(Type::Callable(
CallableType::BoundMethod(BoundMethodType::new(
db,
function,
instance.to_meta_type(db),
)),
));
}
_ => {}
}
} else if let [Some(first), _] = overload.parameter_types() {
if first.is_none(db) {
overload.set_return_type(Type::FunctionLiteral(function));
} else {
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, *first),
)));
}
}
}
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] =
overload.parameter_types()
{
if function.has_known_class_decorator(db, KnownClass::Classmethod)
&& function.decorators(db).len() == 1
{
match overload.parameter_types() {
[_, _, Some(owner)] => {
overload.set_return_type(Type::Callable(
CallableType::BoundMethod(BoundMethodType::new(
db, *function, *owner,
)),
));
}
[_, Some(instance), None] => {
overload.set_return_type(Type::Callable(
CallableType::BoundMethod(BoundMethodType::new(
db,
*function,
instance.to_meta_type(db),
)),
));
}
_ => {}
}
} else {
match overload.parameter_types() {
[_, Some(instance), _] if instance.is_none(db) => {
overload.set_return_type(*function_ty);
}
[_, Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(
type_alias,
))), Some(Type::ClassLiteral(ClassLiteralType { class }))]
if class.is_known(db, KnownClass::TypeAliasType)
&& function.name(db) == "__name__" =>
{
overload.set_return_type(Type::string_literal(
db,
type_alias.name(db),
));
}
[_, Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), Some(Type::ClassLiteral(ClassLiteralType { class }))]
if class.is_known(db, KnownClass::TypeVar)
&& function.name(db) == "__name__" =>
{
overload.set_return_type(Type::string_literal(
db,
typevar.name(db),
));
}
[_, Some(_), _]
if function
.has_known_class_decorator(db, KnownClass::Property) =>
{
overload.set_return_type(todo_type!("@property"));
}
[_, Some(instance), _] => {
overload.set_return_type(Type::Callable(
CallableType::BoundMethod(BoundMethodType::new(
db, *function, *instance,
)),
));
}
_ => {}
}
}
}
}
Type::FunctionLiteral(function_type) => match function_type.known(db) {
Some(KnownFunction::IsEquivalentTo) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_equivalent_to(db, *ty_b),
));
}
}
Some(KnownFunction::IsSubtypeOf) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_subtype_of(db, *ty_b),
));
}
}
Some(KnownFunction::IsAssignableTo) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_assignable_to(db, *ty_b),
));
}
}
Some(KnownFunction::IsDisjointFrom) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_disjoint_from(db, *ty_b),
));
}
}
Some(KnownFunction::IsGradualEquivalentTo) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, *ty_b),
));
}
}
Some(KnownFunction::IsFullyStatic) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
}
}
Some(KnownFunction::IsSingleton) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
}
}
Some(KnownFunction::IsSingleValued) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
}
}
Some(KnownFunction::Len) => {
if let [Some(first_arg)] = overload.parameter_types() {
if let Some(len_ty) = first_arg.len(db) {
overload.set_return_type(len_ty);
}
};
}
Some(KnownFunction::Repr) => {
if let [Some(first_arg)] = overload.parameter_types() {
overload.set_return_type(first_arg.repr(db));
};
}
Some(KnownFunction::Cast) => {
if let [Some(casted_ty), Some(_)] = overload.parameter_types() {
overload.set_return_type(*casted_ty);
}
}
Some(KnownFunction::Overload) => {
overload.set_return_type(todo_type!("overload(..) return type"));
}
Some(KnownFunction::GetattrStatic) => {
let [Some(instance_ty), Some(attr_name), default] =
overload.parameter_types()
else {
continue;
};
let Some(attr_name) = attr_name.into_string_literal() else {
continue;
};
let default = if let Some(default) = default {
*default
} else {
Type::Never
};
let union_with_default = |ty| UnionType::from_elements(db, [ty, default]);
// TODO: we could emit a diagnostic here (if default is not set)
overload.set_return_type(
match instance_ty.static_member(db, attr_name.value(db)) {
Symbol::Type(ty, Boundness::Bound) => {
if instance_ty.is_fully_static(db) {
ty
} else {
// Here, we attempt to model the fact that an attribute lookup on
// a non-fully static type could fail. This is an approximation,
// as there are gradual types like `tuple[Any]`, on which a lookup
// of (e.g. of the `index` method) would always succeed.
union_with_default(ty)
}
}
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
union_with_default(ty)
}
Symbol::Unbound => default,
},
);
}
_ => {}
},
Type::ClassLiteral(ClassLiteralType { class }) => match class.known(db) {
Some(KnownClass::Bool) => match overload.parameter_types() {
[Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)),
[None] => overload.set_return_type(Type::BooleanLiteral(false)),
_ => {}
},
Some(KnownClass::Str) if overload_index == 0 => {
match overload.parameter_types() {
[Some(arg)] => overload.set_return_type(arg.str(db)),
[None] => overload.set_return_type(Type::string_literal(db, "")),
_ => {}
}
}
Some(KnownClass::Type) if overload_index == 0 => {
if let [Some(arg)] = overload.parameter_types() {
overload.set_return_type(arg.to_meta_type(db));
}
}
_ => {}
},
// Not a special case
_ => {}
}
}
}
}
impl<'a, 'db> IntoIterator for &'a Bindings<'db> {
@@ -170,7 +513,7 @@ impl<'a, 'db> IntoIterator for &'a mut Bindings<'db> {
/// overloads, we store this error information for each overload.
///
/// [overloads]: https://github.com/python/typing/pull/1839
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub(crate) struct CallableBinding<'db> {
pub(crate) callable_type: Type<'db>,
pub(crate) signature_type: Type<'db>,
@@ -184,39 +527,55 @@ pub(crate) struct CallableBinding<'db> {
}
impl<'db> CallableBinding<'db> {
/// Bind a [`CallArguments`] against a [`CallableSignature`].
///
/// The returned [`CallableBinding`] provides the return type of the call, the bound types for
/// all parameters, and any errors resulting from binding the call.
fn bind(
db: &'db dyn Db,
fn match_parameters(
signature: &CallableSignature<'db>,
arguments: &CallArguments<'_, 'db>,
arguments: &mut CallArguments<'_>,
argument_forms: &mut [Option<ParameterForm>],
conflicting_forms: &mut [bool],
) -> Self {
// If this callable is a bound method, prepend the self instance onto the arguments list
// before checking.
let arguments = if let Some(bound_type) = signature.bound_type {
Cow::Owned(arguments.with_self(bound_type))
} else {
Cow::Borrowed(arguments)
};
arguments.with_self(signature.bound_type, |arguments| {
// TODO: This checks every overload. In the proposed more detailed call checking spec [1],
// arguments are checked for arity first, and are only checked for type assignability against
// the matching overloads. Make sure to implement that as part of separating call binding into
// two phases.
//
// [1] https://github.com/python/typing/pull/1839
let overloads = signature
.into_iter()
.map(|signature| {
Binding::match_parameters(
signature,
arguments,
argument_forms,
conflicting_forms,
)
})
.collect();
// TODO: This checks every overload. In the proposed more detailed call checking spec [1],
// arguments are checked for arity first, and are only checked for type assignability against
// the matching overloads. Make sure to implement that as part of separating call binding into
// two phases.
//
// [1] https://github.com/python/typing/pull/1839
let overloads = signature
.into_iter()
.map(|signature| Binding::bind(db, signature, arguments.as_ref()))
.collect();
CallableBinding {
callable_type: signature.callable_type,
signature_type: signature.signature_type,
dunder_call_is_possibly_unbound: signature.dunder_call_is_possibly_unbound,
overloads,
}
CallableBinding {
callable_type: signature.callable_type,
signature_type: signature.signature_type,
dunder_call_is_possibly_unbound: signature.dunder_call_is_possibly_unbound,
overloads,
}
})
}
fn check_types(
&mut self,
db: &'db dyn Db,
signature: &CallableSignature<'db>,
argument_types: &mut CallArgumentTypes<'_, 'db>,
) {
// If this callable is a bound method, prepend the self instance onto the arguments list
// before checking.
argument_types.with_self(signature.bound_type, |argument_types| {
for (signature, overload) in signature.iter().zip(&mut self.overloads) {
overload.check_types(db, signature, argument_types);
}
});
}
fn as_result(&self) -> Result<(), CallErrorKind> {
@@ -333,27 +692,35 @@ impl<'db> CallableBinding<'db> {
}
/// Binding information for one of the overloads of a callable.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub(crate) struct Binding<'db> {
/// Return type of the call.
return_ty: Type<'db>,
/// Bound types for parameters, in parameter source order.
parameter_tys: Box<[Type<'db>]>,
/// The formal parameter that each argument is matched with, in argument source order, or
/// `None` if the argument was not matched to any parameter.
argument_parameters: Box<[Option<usize>]>,
/// Bound types for parameters, in parameter source order, or `None` if no argument was matched
/// to that parameter.
parameter_tys: Box<[Option<Type<'db>>]>,
/// Call binding errors, if any.
errors: Vec<BindingError<'db>>,
}
impl<'db> Binding<'db> {
fn bind(
db: &'db dyn Db,
fn match_parameters(
signature: &Signature<'db>,
arguments: &CallArguments<'_, 'db>,
arguments: &CallArguments<'_>,
argument_forms: &mut [Option<ParameterForm>],
conflicting_forms: &mut [bool],
) -> Self {
let parameters = signature.parameters();
// The type assigned to each parameter at this call site.
let mut parameter_tys = vec![None; parameters.len()];
// The parameter that each argument is matched with.
let mut argument_parameters = vec![None; arguments.len()];
// Whether each parameter has been matched with an argument.
let mut parameter_matched = vec![false; parameters.len()];
let mut errors = vec![];
let mut next_positional = 0;
let mut first_excess_positional = None;
@@ -370,9 +737,9 @@ impl<'db> Binding<'db> {
}
};
for (argument_index, argument) in arguments.iter().enumerate() {
let (index, parameter, argument_ty, positional) = match argument {
Argument::Positional(ty) | Argument::Synthetic(ty) => {
if matches!(argument, Argument::Synthetic(_)) {
let (index, parameter, positional) = match argument {
Argument::Positional | Argument::Synthetic => {
if matches!(argument, Argument::Synthetic) {
num_synthetic_args += 1;
}
let Some((index, parameter)) = parameters
@@ -385,9 +752,9 @@ impl<'db> Binding<'db> {
continue;
};
next_positional += 1;
(index, parameter, ty, !parameter.is_variadic())
(index, parameter, !parameter.is_variadic())
}
Argument::Keyword { name, ty } => {
Argument::Keyword(name) => {
let Some((index, parameter)) = parameters
.keyword_by_name(name)
.or_else(|| parameters.keyword_variadic())
@@ -398,35 +765,33 @@ impl<'db> Binding<'db> {
});
continue;
};
(index, parameter, ty, false)
(index, parameter, false)
}
Argument::Variadic(_) | Argument::Keywords(_) => {
Argument::Variadic | Argument::Keywords => {
// TODO
continue;
}
};
if let Some(expected_ty) = parameter.annotated_type() {
if !argument_ty.is_assignable_to(db, expected_ty) {
errors.push(BindingError::InvalidArgumentType {
parameter: ParameterContext::new(parameter, index, positional),
argument_index: get_argument_index(argument_index, num_synthetic_args),
expected_ty,
provided_ty: *argument_ty,
});
if !matches!(argument, Argument::Synthetic) {
if let Some(existing) =
argument_forms[argument_index - num_synthetic_args].replace(parameter.form)
{
if existing != parameter.form {
conflicting_forms[argument_index - num_synthetic_args] = true;
}
}
}
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
if parameter.is_variadic() || parameter.is_keyword_variadic() {
let union = UnionType::from_elements(db, [existing, *argument_ty]);
parameter_tys[index].replace(union);
} else {
if parameter_matched[index] {
if !parameter.is_variadic() && !parameter.is_keyword_variadic() {
errors.push(BindingError::ParameterAlreadyAssigned {
argument_index: get_argument_index(argument_index, num_synthetic_args),
parameter: ParameterContext::new(parameter, index, positional),
});
}
}
argument_parameters[argument_index] = Some(index);
parameter_matched[index] = true;
}
if let Some(first_excess_argument_index) = first_excess_positional {
errors.push(BindingError::TooManyPositionalArguments {
@@ -439,8 +804,8 @@ impl<'db> Binding<'db> {
});
}
let mut missing = vec![];
for (index, bound_ty) in parameter_tys.iter().enumerate() {
if bound_ty.is_none() {
for (index, matched) in parameter_matched.iter().copied().enumerate() {
if !matched {
let param = &parameters[index];
if param.is_variadic()
|| param.is_keyword_variadic()
@@ -461,14 +826,65 @@ impl<'db> Binding<'db> {
Self {
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
parameter_tys: parameter_tys
.into_iter()
.map(|opt_ty| opt_ty.unwrap_or(Type::unknown()))
.collect(),
argument_parameters: argument_parameters.into_boxed_slice(),
parameter_tys: vec![None; parameters.len()].into_boxed_slice(),
errors,
}
}
fn check_types(
&mut self,
db: &'db dyn Db,
signature: &Signature<'db>,
argument_types: &CallArgumentTypes<'_, 'db>,
) {
let parameters = signature.parameters();
let mut num_synthetic_args = 0;
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
if argument_index >= num_synthetic_args {
// Adjust the argument index to skip synthetic args, which don't appear at the call
// site and thus won't be in the Call node arguments list.
Some(argument_index - num_synthetic_args)
} else {
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
// entire Call node, since there's no argument node for this argument at the call site
None
}
};
for (argument_index, (argument, argument_type)) in argument_types.iter().enumerate() {
if matches!(argument, Argument::Synthetic) {
num_synthetic_args += 1;
}
let Some(parameter_index) = self.argument_parameters[argument_index] else {
// There was an error with argument when matching parameters, so don't bother
// type-checking it.
continue;
};
let parameter = &parameters[parameter_index];
if let Some(expected_ty) = parameter.annotated_type() {
if !argument_type.is_assignable_to(db, expected_ty) {
let positional = matches!(argument, Argument::Positional | Argument::Synthetic)
&& !parameter.is_variadic();
self.errors.push(BindingError::InvalidArgumentType {
parameter: ParameterContext::new(parameter, parameter_index, positional),
argument_index: get_argument_index(argument_index, num_synthetic_args),
expected_ty,
provided_ty: argument_type,
});
}
}
// We still update the actual type of the parameter in this binding to match the
// argument, even if the argument type is not assignable to the expected parameter
// type.
if let Some(existing) = self.parameter_tys[parameter_index].replace(argument_type) {
// We already verified in `match_parameters` that we only match multiple arguments
// with variadic parameters.
let union = UnionType::from_elements(db, [existing, argument_type]);
self.parameter_tys[parameter_index] = Some(union);
}
}
}
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
self.return_ty = return_ty;
}
@@ -477,7 +893,7 @@ impl<'db> Binding<'db> {
self.return_ty
}
pub(crate) fn parameter_types(&self) -> &[Type<'db>] {
pub(crate) fn parameter_types(&self) -> &[Option<Type<'db>>] {
&self.parameter_tys
}

View File

@@ -11,7 +11,7 @@ use crate::{
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
},
types::{
definition_expression_type, CallArguments, CallError, CallErrorKind, DynamicType,
definition_expression_type, CallArgumentTypes, CallError, CallErrorKind, DynamicType,
MetaclassCandidate, TupleType, UnionBuilder, UnionType,
},
Db, KnownModule, Program,
@@ -128,9 +128,10 @@ impl<'db> Class<'db> {
#[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)]
fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
tracing::trace!("Class::explicit_bases_query: {}", self.name(db));
let class_stmt = self.node(db);
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
let class_stmt = self.node(db);
let class_definition =
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
class_stmt
.bases()
@@ -156,11 +157,15 @@ impl<'db> Class<'db> {
#[salsa::tracked(return_ref)]
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
tracing::trace!("Class::decorators: {}", self.name(db));
let class_stmt = self.node(db);
if class_stmt.decorator_list.is_empty() {
return Box::new([]);
}
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
let class_definition =
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
class_stmt
.decorator_list
.iter()
@@ -224,9 +229,15 @@ impl<'db> Class<'db> {
.as_ref()?
.find_keyword("metaclass")?
.value;
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
let metaclass_ty = definition_expression_type(db, class_definition, metaclass_node);
Some(metaclass_ty)
let class_definition =
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
Some(definition_expression_type(
db,
class_definition,
metaclass_node,
))
}
/// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred.
@@ -279,13 +290,13 @@ impl<'db> Class<'db> {
let namespace = KnownClass::Dict.to_instance(db);
// TODO: Other keyword arguments?
let arguments = CallArguments::positional([name, bases, namespace]);
let arguments = CallArgumentTypes::positional([name, bases, namespace]);
let return_ty_result = match metaclass.try_call(db, &arguments) {
let return_ty_result = match metaclass.try_call(db, arguments) {
Ok(bindings) => Ok(bindings.return_type(db)),
Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError {
kind: MetaclassErrorKind::NotCallable(bindings.callable_type),
kind: MetaclassErrorKind::NotCallable(bindings.callable_type()),
}),
// TODO we should also check for binding errors that would indicate the metaclass
@@ -834,6 +845,7 @@ pub enum KnownClass {
TypeAliasType,
NoDefaultType,
NewType,
Sized,
// TODO: This can probably be removed when we have support for protocols
SupportsIndex,
// Collections
@@ -911,6 +923,7 @@ impl<'db> KnownClass {
| Self::DefaultDict
| Self::Deque
| Self::Float
| Self::Sized
| Self::Classmethod => Truthiness::Ambiguous,
}
}
@@ -955,6 +968,7 @@ impl<'db> KnownClass {
Self::Counter => "Counter",
Self::DefaultDict => "defaultdict",
Self::Deque => "deque",
Self::Sized => "Sized",
Self::OrderedDict => "OrderedDict",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
Self::StdlibAlias => "_Alias",
@@ -1115,9 +1129,11 @@ impl<'db> KnownClass {
| Self::MethodWrapperType
| Self::WrapperDescriptorType => KnownModule::Types,
Self::NoneType => KnownModule::Typeshed,
Self::SpecialForm | Self::TypeVar | Self::StdlibAlias | Self::SupportsIndex => {
KnownModule::Typing
}
Self::SpecialForm
| Self::TypeVar
| Self::StdlibAlias
| Self::SupportsIndex
| Self::Sized => KnownModule::Typing,
Self::TypeAliasType | Self::TypeVarTuple | Self::ParamSpec | Self::NewType => {
KnownModule::TypingExtensions
}
@@ -1195,6 +1211,7 @@ impl<'db> KnownClass {
| Self::TypeVar
| Self::ParamSpec
| Self::TypeVarTuple
| Self::Sized
| Self::NewType => false,
}
}
@@ -1247,6 +1264,7 @@ impl<'db> KnownClass {
| Self::TypeVar
| Self::ParamSpec
| Self::TypeVarTuple
| Self::Sized
| Self::NewType => false,
}
}
@@ -1299,6 +1317,7 @@ impl<'db> KnownClass {
"_SpecialForm" => Self::SpecialForm,
"_NoDefaultType" => Self::NoDefaultType,
"SupportsIndex" => Self::SupportsIndex,
"Sized" => Self::Sized,
"_version_info" => Self::VersionInfo,
"ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => {
Self::EllipsisType
@@ -1358,6 +1377,7 @@ impl<'db> KnownClass {
| Self::SupportsIndex
| Self::ParamSpec
| Self::TypeVarTuple
| Self::Sized
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
}
}

View File

@@ -7,7 +7,7 @@ use ruff_db::{
};
use ruff_text_size::{Ranged, TextRange};
use super::{binding_type, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics};
use super::{binding_type, KnownFunction, Type, TypeCheckDiagnostic, TypeCheckDiagnostics};
use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::ScopeId;
@@ -177,9 +177,8 @@ impl<'db> InferContext<'db> {
let mut function_scope_tys = index
.ancestor_scopes(scope_id)
.filter_map(|(_, scope)| scope.node().as_function())
.filter_map(|function| {
binding_type(self.db, index.definition(function)).into_function_literal()
});
.map(|node| binding_type(self.db, index.expect_single_definition(node)))
.filter_map(Type::into_function_literal);
// Iterate over all functions and test if any is decorated with `@no_type_check`.
function_scope_tys.any(|function_ty| {

View File

@@ -24,6 +24,7 @@ use std::sync::Arc;
pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&CALL_NON_CALLABLE);
registry.register_lint(&CALL_POSSIBLY_UNBOUND_METHOD);
registry.register_lint(&CONFLICTING_ARGUMENT_FORMS);
registry.register_lint(&CONFLICTING_DECLARATIONS);
registry.register_lint(&CONFLICTING_METACLASS);
registry.register_lint(&CYCLIC_CLASS_DEFINITION);
@@ -106,6 +107,16 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks whether an argument is used as both a value and a type form in a call
pub(crate) static CONFLICTING_ARGUMENT_FORMS = {
summary: "detects when an argument is used as both a value and a type form in a call",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
pub(crate) static CONFLICTING_DECLARATIONS = {

View File

@@ -110,6 +110,9 @@ impl Display for DisplayRepresentation<'_> {
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
f.write_str("<wrapper-descriptor `__get__` of `function` objects>")
}
Type::Callable(CallableType::SpecializedGetitem) => {
f.write_str("<specialized `__getitem__`>")
}
Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
Type::IntLiteral(n) => n.fmt(f),
@@ -292,7 +295,10 @@ impl Display for DisplayUnionType<'_> {
db: self.db,
});
} else {
join.entry(&element.display(self.db));
join.entry(&DisplayMaybeParenthesizedType {
ty: *element,
db: self.db,
});
}
}
@@ -404,7 +410,28 @@ impl Display for DisplayMaybeNegatedType<'_> {
if self.negated {
f.write_str("~")?;
}
self.ty.display(self.db).fmt(f)
DisplayMaybeParenthesizedType {
ty: self.ty,
db: self.db,
}
.fmt(f)
}
}
struct DisplayMaybeParenthesizedType<'db> {
ty: Type<'db>,
db: &'db dyn Db,
}
impl Display for DisplayMaybeParenthesizedType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Type::Callable(CallableType::General(_) | CallableType::MethodWrapperDunderGet(_)) =
self.ty
{
write!(f, "({})", self.ty.display(self.db))
} else {
self.ty.display(self.db).fmt(f)
}
}
}
@@ -476,8 +503,7 @@ mod tests {
use crate::db::tests::setup_db;
use crate::types::{
KnownClass, Parameter, ParameterKind, Parameters, Signature, SliceLiteralType,
StringLiteralType, Type,
KnownClass, Parameter, Parameters, Signature, SliceLiteralType, StringLiteralType, Type,
};
use crate::Db;
@@ -574,13 +600,7 @@ mod tests {
assert_eq!(
display_signature(
&db,
[Parameter::new(
Some(Type::none(&db)),
ParameterKind::PositionalOnly {
name: None,
default_ty: None
}
)],
[Parameter::positional_only(None).with_annotated_type(Type::none(&db))],
Some(Type::none(&db))
),
"(None, /) -> None"
@@ -591,20 +611,11 @@ mod tests {
display_signature(
&db,
[
Parameter::new(
None,
ParameterKind::PositionalOrKeyword {
name: Name::new_static("x"),
default_ty: Some(KnownClass::Int.to_instance(&db))
}
),
Parameter::new(
Some(KnownClass::Str.to_instance(&db)),
ParameterKind::PositionalOrKeyword {
name: Name::new_static("y"),
default_ty: Some(KnownClass::Str.to_instance(&db))
}
)
Parameter::positional_or_keyword(Name::new_static("x"))
.with_default_type(KnownClass::Int.to_instance(&db)),
Parameter::positional_or_keyword(Name::new_static("y"))
.with_annotated_type(KnownClass::Str.to_instance(&db))
.with_default_type(KnownClass::Str.to_instance(&db)),
],
Some(Type::none(&db))
),
@@ -616,20 +627,8 @@ mod tests {
display_signature(
&db,
[
Parameter::new(
None,
ParameterKind::PositionalOnly {
name: Some(Name::new_static("x")),
default_ty: None
}
),
Parameter::new(
None,
ParameterKind::PositionalOnly {
name: Some(Name::new_static("y")),
default_ty: None
}
)
Parameter::positional_only(Some(Name::new_static("x"))),
Parameter::positional_only(Some(Name::new_static("y"))),
],
Some(Type::none(&db))
),
@@ -641,20 +640,8 @@ mod tests {
display_signature(
&db,
[
Parameter::new(
None,
ParameterKind::PositionalOnly {
name: Some(Name::new_static("x")),
default_ty: None
}
),
Parameter::new(
None,
ParameterKind::PositionalOrKeyword {
name: Name::new_static("y"),
default_ty: None
}
)
Parameter::positional_only(Some(Name::new_static("x"))),
Parameter::positional_or_keyword(Name::new_static("y")),
],
Some(Type::none(&db))
),
@@ -666,20 +653,8 @@ mod tests {
display_signature(
&db,
[
Parameter::new(
None,
ParameterKind::KeywordOnly {
name: Name::new_static("x"),
default_ty: None
}
),
Parameter::new(
None,
ParameterKind::KeywordOnly {
name: Name::new_static("y"),
default_ty: None
}
)
Parameter::keyword_only(Name::new_static("x")),
Parameter::keyword_only(Name::new_static("y")),
],
Some(Type::none(&db))
),
@@ -691,20 +666,8 @@ mod tests {
display_signature(
&db,
[
Parameter::new(
None,
ParameterKind::PositionalOrKeyword {
name: Name::new_static("x"),
default_ty: None
}
),
Parameter::new(
None,
ParameterKind::KeywordOnly {
name: Name::new_static("y"),
default_ty: None
}
)
Parameter::positional_or_keyword(Name::new_static("x")),
Parameter::keyword_only(Name::new_static("y")),
],
Some(Type::none(&db))
),
@@ -716,74 +679,28 @@ mod tests {
display_signature(
&db,
[
Parameter::new(
None,
ParameterKind::PositionalOnly {
name: Some(Name::new_static("a")),
default_ty: None
},
),
Parameter::new(
Some(KnownClass::Int.to_instance(&db)),
ParameterKind::PositionalOnly {
name: Some(Name::new_static("b")),
default_ty: None
},
),
Parameter::new(
None,
ParameterKind::PositionalOnly {
name: Some(Name::new_static("c")),
default_ty: Some(Type::IntLiteral(1)),
},
),
Parameter::new(
Some(KnownClass::Int.to_instance(&db)),
ParameterKind::PositionalOnly {
name: Some(Name::new_static("d")),
default_ty: Some(Type::IntLiteral(2)),
},
),
Parameter::new(
None,
ParameterKind::PositionalOrKeyword {
name: Name::new_static("e"),
default_ty: Some(Type::IntLiteral(3)),
},
),
Parameter::new(
Some(KnownClass::Int.to_instance(&db)),
ParameterKind::PositionalOrKeyword {
name: Name::new_static("f"),
default_ty: Some(Type::IntLiteral(4)),
},
),
Parameter::new(
Some(Type::object(&db)),
ParameterKind::Variadic {
name: Name::new_static("args")
},
),
Parameter::new(
None,
ParameterKind::KeywordOnly {
name: Name::new_static("g"),
default_ty: Some(Type::IntLiteral(5)),
},
),
Parameter::new(
Some(KnownClass::Int.to_instance(&db)),
ParameterKind::KeywordOnly {
name: Name::new_static("h"),
default_ty: Some(Type::IntLiteral(6)),
},
),
Parameter::new(
Some(KnownClass::Str.to_instance(&db)),
ParameterKind::KeywordVariadic {
name: Name::new_static("kwargs")
},
),
Parameter::positional_only(Some(Name::new_static("a"))),
Parameter::positional_only(Some(Name::new_static("b")))
.with_annotated_type(KnownClass::Int.to_instance(&db)),
Parameter::positional_only(Some(Name::new_static("c")))
.with_default_type(Type::IntLiteral(1)),
Parameter::positional_only(Some(Name::new_static("d")))
.with_annotated_type(KnownClass::Int.to_instance(&db))
.with_default_type(Type::IntLiteral(2)),
Parameter::positional_or_keyword(Name::new_static("e"))
.with_default_type(Type::IntLiteral(3)),
Parameter::positional_or_keyword(Name::new_static("f"))
.with_annotated_type(KnownClass::Int.to_instance(&db))
.with_default_type(Type::IntLiteral(4)),
Parameter::variadic(Name::new_static("args"))
.with_annotated_type(Type::object(&db)),
Parameter::keyword_only(Name::new_static("g"))
.with_default_type(Type::IntLiteral(5)),
Parameter::keyword_only(Name::new_static("h"))
.with_annotated_type(KnownClass::Int.to_instance(&db))
.with_default_type(Type::IntLiteral(6)),
Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(KnownClass::Str.to_instance(&db)),
],
Some(KnownClass::Bytes.to_instance(&db))
),

View File

@@ -33,8 +33,6 @@
//! the query cycle until a fixed-point is reached. Salsa has a built-in fixed limit on the number
//! of iterations, so if we fail to converge, Salsa will eventually panic. (This should of course
//! be considered a bug.)
use std::num::NonZeroU32;
use itertools::{Either, Itertools};
use ruff_db::diagnostic::{DiagnosticId, Severity};
use ruff_db::files::File;
@@ -45,8 +43,8 @@ use rustc_hash::{FxHashMap, FxHashSet};
use salsa;
use salsa::plumbing::AsId;
use crate::module_name::ModuleName;
use crate::module_resolver::{file_to_module, resolve_module};
use crate::module_name::{ModuleName, ModuleNameResolutionError};
use crate::module_resolver::resolve_module;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId};
use crate::semantic_index::definition::{
AssignmentDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey,
@@ -54,14 +52,16 @@ use crate::semantic_index::definition::{
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::{FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId};
use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind,
};
use crate::semantic_index::SemanticIndex;
use crate::symbol::{
builtins_module_scope, builtins_symbol, explicit_global_symbol,
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
typing_extensions_symbol, Boundness, LookupError,
};
use crate::types::call::{Argument, CallArguments, CallError};
use crate::types::call::{Argument, Bindings, CallArgumentTypes, CallArguments, CallError};
use crate::types::diagnostic::{
report_implicit_return_type, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
@@ -79,12 +79,12 @@ use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType,
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
MetaclassCandidate, Parameter, Parameters, SliceLiteralType, SubclassOfType, Symbol,
SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
MetaclassCandidate, Parameter, ParameterForm, Parameters, SliceLiteralType, SubclassOfType,
Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
UnionType,
};
use crate::types::{CallableType, GeneralCallableType, ParameterKind, Signature};
use crate::types::{CallableType, GeneralCallableType, Signature};
use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
@@ -102,7 +102,7 @@ use super::slots::check_class_slots;
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
use super::{CallDunderError, ParameterExpectation, ParameterExpectations};
use super::CallDunderError;
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@@ -491,7 +491,7 @@ enum DeclaredAndInferredType<'db> {
/// [`TypeInferenceBuilder`] just for that definition, and we merge the returned [`TypeInference`]
/// into the one we are currently building for the entire scope. Using the query in this way
/// ensures that if we first infer types for some scattered definitions in a scope, and later for
/// the entire scope, we don't re-infer any types, we re-use the cached inference for those
/// the entire scope, we don't re-infer any types, we reuse the cached inference for those
/// definitions and their sub-expressions.
///
/// Functions with a name like `infer_*_definition` take both a node and a [`Definition`], and are
@@ -887,6 +887,9 @@ impl<'db> TypeInferenceBuilder<'db> {
definition,
);
}
DefinitionKind::StarImport(import) => {
self.infer_import_from_definition(import.import(), import.alias(), definition);
}
DefinitionKind::Assignment(assignment) => {
self.infer_assignment_definition(assignment, definition);
}
@@ -1141,7 +1144,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_type_parameters(type_params);
if let Some(arguments) = class.arguments.as_deref() {
self.infer_arguments(arguments, ParameterExpectations::default());
let call_arguments = Self::parse_arguments(arguments);
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
self.infer_argument_types(arguments, call_arguments, &argument_forms);
}
}
@@ -1205,15 +1210,57 @@ impl<'db> TypeInferenceBuilder<'db> {
_ => false,
}
}
let is_overload = function.decorator_list.iter().any(|decorator| {
let is_overload_or_abstract = function.decorator_list.iter().any(|decorator| {
let decorator_type = self.file_expression_type(&decorator.expression);
decorator_type
.into_function_literal()
.is_some_and(|f| f.is_known(self.db(), KnownFunction::Overload))
match decorator_type {
Type::FunctionLiteral(function) => matches!(
function.known(self.db()),
Some(KnownFunction::Overload | KnownFunction::AbstractMethod)
),
_ => false,
}
});
// TODO: Protocol / abstract methods can have empty bodies
if (self.in_stub() || is_overload)
let class_inherits_protocol_directly = (|| -> bool {
let current_scope_id = self.scope().file_scope_id(self.db());
let current_scope = self.index.scope(current_scope_id);
let Some(parent_scope_id) = current_scope.parent() else {
return false;
};
let parent_scope = self.index.scope(parent_scope_id);
let class_scope = match parent_scope.kind() {
ScopeKind::Class => parent_scope,
ScopeKind::Annotation => {
let Some(class_scope_id) = parent_scope.parent() else {
return false;
};
let potentially_class_scope = self.index.scope(class_scope_id);
match potentially_class_scope.kind() {
ScopeKind::Class => potentially_class_scope,
_ => return false,
}
}
_ => return false,
};
let NodeWithScopeKind::Class(node_ref) = class_scope.node() else {
return false;
};
// TODO move this to `Class` once we add proper `Protocol` support
node_ref.bases().iter().any(|base| {
matches!(
self.file_expression_type(base),
Type::KnownInstance(KnownInstanceType::Protocol)
)
})
})();
if (self.in_stub() || is_overload_or_abstract || class_inherits_protocol_directly)
&& self.return_types_and_ranges.is_empty()
&& is_stub_suite(&function.body)
{
@@ -1290,8 +1337,8 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
fn infer_definition(&mut self, node: impl Into<DefinitionNodeKey>) {
let definition = self.index.definition(node);
fn infer_definition(&mut self, node: impl Into<DefinitionNodeKey> + std::fmt::Debug + Copy) {
let definition = self.index.expect_single_definition(node);
let result = infer_definition_types(self.db(), definition);
self.extend(result);
}
@@ -1517,7 +1564,7 @@ impl<'db> TypeInferenceBuilder<'db> {
) {
if let Some(annotation) = parameter.annotation() {
let _annotated_ty = self.file_expression_type(annotation);
// TODO `tuple[annotated_ty, ...]`
// TODO `tuple[annotated_type, ...]`
let ty = KnownClass::Tuple.to_instance(self.db());
self.add_declaration_with_binding(
parameter.into(),
@@ -1548,7 +1595,7 @@ impl<'db> TypeInferenceBuilder<'db> {
) {
if let Some(annotation) = parameter.annotation() {
let _annotated_ty = self.file_expression_type(annotation);
// TODO `dict[str, annotated_ty]`
// TODO `dict[str, annotated_type]`
let ty = KnownClass::Dict.to_instance(self.db());
self.add_declaration_with_binding(
parameter.into(),
@@ -2276,7 +2323,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let successful_call = meta_dunder_set
.try_call(
db,
&CallArguments::positional([meta_attr_ty, object_ty, value_ty]),
CallArgumentTypes::positional([meta_attr_ty, object_ty, value_ty]),
)
.is_ok();
@@ -2375,7 +2422,11 @@ impl<'db> TypeInferenceBuilder<'db> {
let successful_call = meta_dunder_set
.try_call(
db,
&CallArguments::positional([meta_attr_ty, object_ty, value_ty]),
CallArgumentTypes::positional([
meta_attr_ty,
object_ty,
value_ty,
]),
)
.is_ok();
@@ -2783,7 +2834,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let call = target_type.try_call_dunder(
db,
op.in_place_dunder(),
&CallArguments::positional([value_type]),
CallArgumentTypes::positional([value_type]),
);
match call {
@@ -2977,7 +3028,18 @@ impl<'db> TypeInferenceBuilder<'db> {
} = import;
for alias in names {
self.infer_definition(alias);
let definitions = self.index.definitions(alias);
if definitions.is_empty() {
// If the module couldn't be resolved while constructing the semantic index,
// this node won't have any definitions associated with it -- but we need to
// make sure that we still emit the diagnostic for the unresolvable module,
// since this will cause the import to fail at runtime.
self.resolve_import_from_module(import, alias);
} else {
for definition in definitions {
self.extend(infer_definition_types(self.db(), *definition));
}
}
}
}
@@ -3029,52 +3091,13 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
/// Given a `from .foo import bar` relative import, resolve the relative module
/// we're importing `bar` from into an absolute [`ModuleName`]
/// using the name of the module we're currently analyzing.
///
/// - `level` is the number of dots at the beginning of the relative module name:
/// - `from .foo.bar import baz` => `level == 1`
/// - `from ...foo.bar import baz` => `level == 3`
/// - `tail` is the relative module name stripped of all leading dots:
/// - `from .foo import bar` => `tail == "foo"`
/// - `from ..foo.bar import baz` => `tail == "foo.bar"`
fn relative_module_name(
&self,
tail: Option<&str>,
level: NonZeroU32,
) -> Result<ModuleName, ModuleNameResolutionError> {
let module = file_to_module(self.db(), self.file())
.ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
let mut level = level.get();
if module.kind().is_package() {
level = level.saturating_sub(1);
}
let mut module_name = module
.name()
.ancestors()
.nth(level as usize)
.ok_or(ModuleNameResolutionError::TooManyDots)?;
if let Some(tail) = tail {
let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
module_name.extend(&tail);
}
Ok(module_name)
}
fn infer_import_from_definition(
/// Resolve the [`ModuleName`], and the type of the module, being referred to by an
/// [`ast::StmtImportFrom`] node. Emit a diagnostic if the module cannot be resolved.
fn resolve_import_from_module(
&mut self,
import_from: &'db ast::StmtImportFrom,
import_from: &ast::StmtImportFrom,
alias: &ast::Alias,
definition: Definition<'db>,
) {
// TODO:
// - Absolute `*` imports (`from collections import *`)
// - Relative `*` imports (`from ...foo import *`)
) -> Option<(ModuleName, Type<'db>)> {
let ast::StmtImportFrom { module, level, .. } = import_from;
// For diagnostics, we want to highlight the unresolvable
// module and not the entire `from ... import ...` statement.
@@ -3084,32 +3107,20 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_or_else(|| AnyNodeRef::from(import_from));
let module = module.as_deref();
let module_name = if let Some(level) = NonZeroU32::new(*level) {
tracing::trace!(
"Resolving imported object `{}` from module `{}` relative to file `{}`",
alias.name,
format_import_from_module(level.get(), module),
self.file().path(self.db()),
);
self.relative_module_name(module, level)
} else {
tracing::trace!(
"Resolving imported object `{}` from module `{}`",
alias.name,
format_import_from_module(*level, module),
);
module
.and_then(ModuleName::new)
.ok_or(ModuleNameResolutionError::InvalidSyntax)
};
tracing::trace!(
"Resolving imported object `{}` from module `{}` into file `{}`",
alias.name,
format_import_from_module(*level, module),
self.file().path(self.db()),
);
let module_name = ModuleName::from_import_statement(self.db(), self.file(), import_from);
let module_name = match module_name {
Ok(module_name) => module_name,
Err(ModuleNameResolutionError::InvalidSyntax) => {
tracing::debug!("Failed to resolve import due to invalid syntax");
// Invalid syntax diagnostics are emitted elsewhere.
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
return None;
}
Err(ModuleNameResolutionError::TooManyDots) => {
tracing::debug!(
@@ -3117,8 +3128,7 @@ impl<'db> TypeInferenceBuilder<'db> {
format_import_from_module(*level, module),
);
report_unresolved_module(&self.context, module_ref, *level, module);
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
return None;
}
Err(ModuleNameResolutionError::UnknownCurrentModule) => {
tracing::debug!(
@@ -3127,26 +3137,51 @@ impl<'db> TypeInferenceBuilder<'db> {
self.file().path(self.db())
);
report_unresolved_module(&self.context, module_ref, *level, module);
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
return None;
}
};
let Some(module_ty) = self.module_type_from_name(&module_name) else {
report_unresolved_module(&self.context, module_ref, *level, module);
return None;
};
Some((module_name, module_ty))
}
fn infer_import_from_definition(
&mut self,
import_from: &'db ast::StmtImportFrom,
alias: &ast::Alias,
definition: Definition<'db>,
) {
let Some((module_name, module_ty)) = self.resolve_import_from_module(import_from, alias)
else {
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
};
let ast::Alias {
range: _,
name,
asname: _,
} = alias;
// The indirection of having `star_import_info` as a separate variable
// is required in order to make the borrow checker happy.
let star_import_info = definition
.kind(self.db())
.as_star_import()
.map(|star_import| {
let symbol_table = self
.index
.symbol_table(self.scope().file_scope_id(self.db()));
(star_import, symbol_table)
});
let name = if let Some((star_import, symbol_table)) = star_import_info.as_ref() {
symbol_table.symbol(star_import.symbol_id()).name()
} else {
&alias.name.id
};
// First try loading the requested attribute from the module.
if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), &name.id).symbol {
if boundness == Boundness::PossiblyUnbound {
if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), name).symbol {
if &alias.name != "*" && boundness == Boundness::PossiblyUnbound {
// TODO: Consider loading _both_ the attribute and any submodule and unioning them
// together if the attribute exists but is possibly-unbound.
self.context.report_lint(
@@ -3191,11 +3226,14 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
self.context.report_lint(
&UNRESOLVED_IMPORT,
AnyNodeRef::Alias(alias),
format_args!("Module `{module_name}` has no member `{name}`",),
);
if &alias.name != "*" {
self.context.report_lint(
&UNRESOLVED_IMPORT,
AnyNodeRef::Alias(alias),
format_args!("Module `{module_name}` has no member `{name}`",),
);
}
self.add_unknown_declaration_with_binding(alias.into(), definition);
}
@@ -3232,45 +3270,22 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_expression(expression)
}
fn infer_arguments<'a>(
&mut self,
arguments: &'a ast::Arguments,
parameter_expectations: ParameterExpectations,
) -> CallArguments<'a, 'db> {
fn parse_arguments(arguments: &ast::Arguments) -> CallArguments<'_> {
arguments
.arguments_source_order()
.enumerate()
.map(|(index, arg_or_keyword)| {
let infer_argument_type = match parameter_expectations.expectation_at_index(index) {
ParameterExpectation::TypeExpression => Self::infer_type_expression,
ParameterExpectation::ValueExpression => Self::infer_expression,
};
.map(|arg_or_keyword| {
match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred {
value,
range: _,
ctx: _,
}) => {
let ty = infer_argument_type(self, value);
self.store_expression_type(arg, ty);
Argument::Variadic(ty)
}
ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic,
// TODO diagnostic if after a keyword argument
_ => Argument::Positional(infer_argument_type(self, arg)),
_ => Argument::Positional,
},
ast::ArgOrKeyword::Keyword(ast::Keyword {
arg,
value,
range: _,
}) => {
let ty = infer_argument_type(self, value);
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => {
if let Some(arg) = arg {
Argument::Keyword { name: &arg.id, ty }
Argument::Keyword(&arg.id)
} else {
// TODO diagnostic if not last
Argument::Keywords(ty)
Argument::Keywords
}
}
}
@@ -3278,6 +3293,44 @@ impl<'db> TypeInferenceBuilder<'db> {
.collect()
}
fn infer_argument_types<'a>(
&mut self,
ast_arguments: &ast::Arguments,
arguments: CallArguments<'a>,
argument_forms: &[Option<ParameterForm>],
) -> CallArgumentTypes<'a, 'db> {
let mut ast_arguments = ast_arguments.arguments_source_order();
CallArgumentTypes::new(arguments, |index, _| {
let arg_or_keyword = ast_arguments
.next()
.expect("argument lists should have consistent lengths");
match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
let ty = self.infer_argument_type(value, argument_forms[index]);
self.store_expression_type(arg, ty);
ty
}
_ => self.infer_argument_type(arg, argument_forms[index]),
},
ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => {
self.infer_argument_type(value, argument_forms[index])
}
}
})
}
fn infer_argument_type(
&mut self,
ast_argument: &ast::Expr,
form: Option<ParameterForm>,
) -> Type<'db> {
match form {
None | Some(ParameterForm::Value) => self.infer_expression(ast_argument),
Some(ParameterForm::Type) => self.infer_type_expression(ast_argument),
}
}
fn infer_optional_expression(&mut self, expression: Option<&ast::Expr>) -> Option<Type<'db>> {
expression.map(|expr| self.infer_expression(expr))
}
@@ -3701,7 +3754,7 @@ impl<'db> TypeInferenceBuilder<'db> {
fn infer_named_expression(&mut self, named: &ast::ExprNamed) -> Type<'db> {
// See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements
if named.target.is_name_expr() {
let definition = self.index.definition(named);
let definition = self.index.expect_single_definition(named);
let result = infer_definition_types(self.db(), definition);
self.extend(result);
result.binding_type(definition)
@@ -3769,64 +3822,44 @@ impl<'db> TypeInferenceBuilder<'db> {
let positional_only = parameters
.posonlyargs
.iter()
.map(|parameter| {
Parameter::new(
None,
ParameterKind::PositionalOnly {
name: Some(parameter.name().id.clone()),
default_ty: parameter
.default()
.map(|default| self.infer_expression(default)),
},
)
.map(|param| {
let mut parameter = Parameter::positional_only(Some(param.name().id.clone()));
if let Some(default) = param.default() {
parameter = parameter.with_default_type(self.infer_expression(default));
}
parameter
})
.collect::<Vec<_>>();
let positional_or_keyword = parameters
.args
.iter()
.map(|parameter| {
Parameter::new(
None,
ParameterKind::PositionalOrKeyword {
name: parameter.name().id.clone(),
default_ty: parameter
.default()
.map(|default| self.infer_expression(default)),
},
)
.map(|param| {
let mut parameter = Parameter::positional_or_keyword(param.name().id.clone());
if let Some(default) = param.default() {
parameter = parameter.with_default_type(self.infer_expression(default));
}
parameter
})
.collect::<Vec<_>>();
let variadic = parameters.vararg.as_ref().map(|parameter| {
Parameter::new(
None,
ParameterKind::Variadic {
name: parameter.name.id.clone(),
},
)
});
let variadic = parameters
.vararg
.as_ref()
.map(|param| Parameter::variadic(param.name().id.clone()));
let keyword_only = parameters
.kwonlyargs
.iter()
.map(|parameter| {
Parameter::new(
None,
ParameterKind::KeywordOnly {
name: parameter.name().id.clone(),
default_ty: parameter
.default()
.map(|default| self.infer_expression(default)),
},
)
.map(|param| {
let mut parameter = Parameter::keyword_only(param.name().id.clone());
if let Some(default) = param.default() {
parameter = parameter.with_default_type(self.infer_expression(default));
}
parameter
})
.collect::<Vec<_>>();
let keyword_variadic = parameters.kwarg.as_ref().map(|parameter| {
Parameter::new(
None,
ParameterKind::KeywordVariadic {
name: parameter.name.id.clone(),
},
)
});
let keyword_variadic = parameters
.kwarg
.as_ref()
.map(|param| Parameter::keyword_variadic(param.name().id.clone()));
Parameters::new(
positional_only
@@ -3856,16 +3889,17 @@ impl<'db> TypeInferenceBuilder<'db> {
arguments,
} = call_expression;
// We don't call `Type::try_call`, because we want to perform type inference on the
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
let mut call_arguments = Self::parse_arguments(arguments);
let function_type = self.infer_expression(func);
let signatures = function_type.signatures(self.db());
let bindings = Bindings::match_parameters(signatures, &mut call_arguments);
let mut call_argument_types =
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
let parameter_expectations = function_type
.into_function_literal()
.and_then(|f| f.known(self.db()))
.map(KnownFunction::parameter_expectations)
.unwrap_or_default();
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
match function_type.try_call(self.db(), &call_arguments) {
match bindings.check_types(self.db(), &mut call_argument_types) {
Ok(bindings) => {
for binding in &bindings {
let Some(known_function) = binding
@@ -3882,7 +3916,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match known_function {
KnownFunction::RevealType => {
if let [revealed_type] = overload.parameter_types() {
if let [Some(revealed_type)] = overload.parameter_types() {
self.context.report_diagnostic(
call_expression,
DiagnosticId::RevealedType,
@@ -3896,7 +3930,8 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
KnownFunction::AssertType => {
if let [actual_ty, asserted_ty] = overload.parameter_types() {
if let [Some(actual_ty), Some(asserted_ty)] = overload.parameter_types()
{
if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) {
self.context.report_lint(
&TYPE_ASSERTION_FAILURE,
@@ -3911,7 +3946,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
KnownFunction::StaticAssert => {
if let [parameter_ty, message] = overload.parameter_types() {
if let [Some(parameter_ty), message] = overload.parameter_types() {
let truthiness = match parameter_ty.try_bool(self.db()) {
Ok(truthiness) => truthiness,
Err(err) => {
@@ -3934,8 +3969,9 @@ impl<'db> TypeInferenceBuilder<'db> {
};
if !truthiness.is_always_true() {
if let Some(message) =
message.into_string_literal().map(|s| &**s.value(self.db()))
if let Some(message) = message
.and_then(Type::into_string_literal)
.map(|s| &**s.value(self.db()))
{
self.context.report_lint(
&STATIC_ASSERT_ERROR,
@@ -4352,7 +4388,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match operand_type.try_call_dunder(
self.db(),
unary_dunder_method,
&CallArguments::none(),
CallArgumentTypes::none(),
) {
Ok(outcome) => outcome.return_type(self.db()),
Err(e) => {
@@ -4634,7 +4670,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
self.db(),
reflected_dunder,
&CallArguments::positional([left_ty]),
CallArgumentTypes::positional([left_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
.or_else(|_| {
@@ -4642,7 +4678,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
self.db(),
op.dunder(),
&CallArguments::positional([right_ty]),
CallArgumentTypes::positional([right_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
})
@@ -4654,7 +4690,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
self.db(),
op.dunder(),
&CallArguments::positional([right_ty]),
CallArgumentTypes::positional([right_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
.ok();
@@ -4667,7 +4703,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
self.db(),
op.reflected_dunder(),
&CallArguments::positional([left_ty]),
CallArgumentTypes::positional([left_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
.ok()
@@ -5318,7 +5354,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
db,
op.dunder(),
&CallArguments::positional([Type::Instance(right)]),
CallArgumentTypes::positional([Type::Instance(right)]),
)
.map(|outcome| outcome.return_type(db))
.ok()
@@ -5367,7 +5403,10 @@ impl<'db> TypeInferenceBuilder<'db> {
contains_dunder
.try_call(
db,
&CallArguments::positional([Type::Instance(right), Type::Instance(left)]),
CallArgumentTypes::positional([
Type::Instance(right),
Type::Instance(left),
]),
)
.map(|bindings| bindings.return_type(db))
.ok()
@@ -5504,20 +5543,6 @@ impl<'db> TypeInferenceBuilder<'db> {
slice_ty: Type<'db>,
) -> Type<'db> {
match (value_ty, slice_ty) {
(
Type::Instance(instance),
Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_),
) if instance
.class()
.is_known(self.db(), KnownClass::VersionInfo) =>
{
self.infer_subscript_expression_types(
value_node,
Type::version_info_tuple(self.db()),
slice_ty,
)
}
// Ex) Given `("a", "b", "c", "d")[1]`, return `"b"`
(Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => {
let elements = tuple_ty.elements(self.db());
@@ -5643,7 +5668,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match value_ty.try_call_dunder(
self.db(),
"__getitem__",
&CallArguments::positional([slice_ty]),
CallArgumentTypes::positional([slice_ty]),
) {
Ok(outcome) => return outcome.return_type(self.db()),
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
@@ -5659,12 +5684,13 @@ impl<'db> TypeInferenceBuilder<'db> {
return err.fallback_return_type(self.db());
}
Err(CallDunderError::CallError(_, bindings)) => {
bindings.report_diagnostics(&self.context, value_node.into());
self.context.report_lint(
&CALL_NON_CALLABLE,
value_node,
format_args!(
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
bindings.callable_type.display(self.db()),
bindings.callable_type().display(self.db()),
value_ty.display(self.db()),
),
);
@@ -5705,7 +5731,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match ty.try_call(
self.db(),
&CallArguments::positional([value_ty, slice_ty]),
CallArgumentTypes::positional([value_ty, slice_ty]),
) {
Ok(bindings) => return bindings.return_type(self.db()),
Err(CallError(_, bindings)) => {
@@ -5714,7 +5740,7 @@ impl<'db> TypeInferenceBuilder<'db> {
value_node,
format_args!(
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
bindings.callable_type.display(self.db()),
bindings.callable_type().display(self.db()),
value_ty.display(self.db()),
),
);
@@ -6974,13 +7000,7 @@ impl<'db> TypeInferenceBuilder<'db> {
Parameters::todo()
} else {
Parameters::new(parameter_types.iter().map(|param_type| {
Parameter::new(
Some(*param_type),
ParameterKind::PositionalOnly {
name: None,
default_ty: None,
},
)
Parameter::positional_only(None).with_annotated_type(*param_type)
}))
}
}
@@ -7148,23 +7168,6 @@ fn format_import_from_module(level: u32, module: Option<&str>) -> String {
)
}
/// Various ways in which resolving a [`ModuleName`]
/// from an [`ast::StmtImport`] or [`ast::StmtImportFrom`] node might fail
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ModuleNameResolutionError {
/// The import statement has invalid syntax
InvalidSyntax,
/// We couldn't resolve the file we're currently analyzing back to a module
/// (Only necessary for relative import statements)
UnknownCurrentModule,
/// The relative import statement seems to take us outside of the module search path
/// (e.g. our current module is `foo.bar`, and the relative import statement in `foo.bar`
/// is `from ....baz import spam`)
TooManyDots,
}
/// Struct collecting string parts when inferring a formatted string. Infers a string literal if the
/// concatenated string is small enough, otherwise infers a literal string.
///

View File

@@ -103,12 +103,6 @@ mod stable {
forall types s, t. s.is_subtype_of(db, t) && t.is_subtype_of(db, s) => s.is_equivalent_to(db, t)
);
// If `S <: T`, then `~T <: ~S`.
type_property_test!(
negation_reverses_subtype_order, db,
forall types s, t. s.is_subtype_of(db, t) => t.negate(db).is_subtype_of(db, s.negate(db))
);
// `T` is not disjoint from itself, unless `T` is `Never`.
type_property_test!(
disjoint_from_is_irreflexive, db,
@@ -266,7 +260,7 @@ mod flaky {
);
// Equal element sets of unions implies equivalence
// flaky at laest in part because of https://github.com/astral-sh/ruff/issues/15513
// flaky at least in part because of https://github.com/astral-sh/ruff/issues/15513
type_property_test!(
union_equivalence_not_order_dependent, db,
forall types s, t, u.
@@ -286,4 +280,19 @@ mod flaky {
forall types s, t.
!s.is_disjoint_from(db, union(db, [s, t])) && !t.is_disjoint_from(db, union(db, [s, t]))
);
// If `S <: T`, then `~T <: ~S`.
//
// DO NOT STABILISE this test until the mdtests here pass:
// https://github.com/astral-sh/ruff/blob/2711e08eb8eb38d1ce323aae0517fede371cba15/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md?plain=1#L276-L315
//
// This test has flakes relating to those subtyping and simplification tests
// (see https://github.com/astral-sh/ruff/issues/16913), but it is hard to
// reliably trigger the flakes when running this test manually as the flakes
// occur very rarely (even running the test with several million seeds does
// not always reliably reproduce the flake).
type_property_test!(
negation_reverses_subtype_order, db,
forall types s, t. s.is_subtype_of(db, t) => t.negate(db).is_subtype_of(db, s.negate(db))
);
}

View File

@@ -66,6 +66,10 @@ impl<'db> Signatures<'db> {
}
}
pub(crate) fn iter(&self) -> std::slice::Iter<'_, CallableSignature<'db>> {
self.elements.iter()
}
pub(crate) fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) {
if self.callable_type == before {
self.callable_type = after;
@@ -87,7 +91,7 @@ impl<'a, 'db> IntoIterator for &'a Signatures<'db> {
type IntoIter = std::slice::Iter<'a, CallableSignature<'db>>;
fn into_iter(self) -> Self::IntoIter {
self.elements.iter()
self.iter()
}
}
@@ -179,6 +183,10 @@ impl<'db> CallableSignature<'db> {
self
}
pub(crate) fn iter(&self) -> std::slice::Iter<'_, Signature<'db>> {
self.overloads.iter()
}
fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) {
if self.callable_type == before {
self.callable_type = after;
@@ -191,7 +199,7 @@ impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> {
type IntoIter = std::slice::Iter<'a, Signature<'db>>;
fn into_iter(self) -> Self::IntoIter {
self.overloads.iter()
self.iter()
}
}
@@ -306,18 +314,10 @@ impl<'db> Parameters<'db> {
pub(crate) fn todo() -> Self {
Self {
value: vec![
Parameter {
annotated_ty: Some(todo_type!("todo signature *args")),
kind: ParameterKind::Variadic {
name: Name::new_static("args"),
},
},
Parameter {
annotated_ty: Some(todo_type!("todo signature **kwargs")),
kind: ParameterKind::KeywordVariadic {
name: Name::new_static("kwargs"),
},
},
Parameter::variadic(Name::new_static("args"))
.with_annotated_type(todo_type!("todo signature *args")),
Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(todo_type!("todo signature **kwargs")),
],
is_gradual: false,
}
@@ -331,18 +331,10 @@ impl<'db> Parameters<'db> {
pub(crate) fn gradual_form() -> Self {
Self {
value: vec![
Parameter {
annotated_ty: Some(Type::Dynamic(DynamicType::Any)),
kind: ParameterKind::Variadic {
name: Name::new_static("args"),
},
},
Parameter {
annotated_ty: Some(Type::Dynamic(DynamicType::Any)),
kind: ParameterKind::KeywordVariadic {
name: Name::new_static("kwargs"),
},
},
Parameter::variadic(Name::new_static("args"))
.with_annotated_type(Type::Dynamic(DynamicType::Any)),
Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(Type::Dynamic(DynamicType::Any)),
],
is_gradual: true,
}
@@ -357,18 +349,10 @@ impl<'db> Parameters<'db> {
pub(crate) fn unknown() -> Self {
Self {
value: vec![
Parameter {
annotated_ty: Some(Type::Dynamic(DynamicType::Unknown)),
kind: ParameterKind::Variadic {
name: Name::new_static("args"),
},
},
Parameter {
annotated_ty: Some(Type::Dynamic(DynamicType::Unknown)),
kind: ParameterKind::KeywordVariadic {
name: Name::new_static("kwargs"),
},
},
Parameter::variadic(Name::new_static("args"))
.with_annotated_type(Type::Dynamic(DynamicType::Unknown)),
Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(Type::Dynamic(DynamicType::Unknown)),
],
is_gradual: true,
}
@@ -387,7 +371,7 @@ impl<'db> Parameters<'db> {
kwarg,
range: _,
} = parameters;
let default_ty = |param: &ast::ParameterWithDefault| {
let default_type = |param: &ast::ParameterWithDefault| {
param
.default()
.map(|default| definition_expression_type(db, definition, default))
@@ -399,7 +383,7 @@ impl<'db> Parameters<'db> {
&arg.parameter,
ParameterKind::PositionalOnly {
name: Some(arg.parameter.name.id.clone()),
default_ty: default_ty(arg),
default_type: default_type(arg),
},
)
});
@@ -410,7 +394,7 @@ impl<'db> Parameters<'db> {
&arg.parameter,
ParameterKind::PositionalOrKeyword {
name: arg.parameter.name.id.clone(),
default_ty: default_ty(arg),
default_type: default_type(arg),
},
)
});
@@ -431,7 +415,7 @@ impl<'db> Parameters<'db> {
&arg.parameter,
ParameterKind::KeywordOnly {
name: arg.parameter.name.id.clone(),
default_ty: default_ty(arg),
default_type: default_type(arg),
},
)
});
@@ -531,14 +515,82 @@ impl<'db> std::ops::Index<usize> for Parameters<'db> {
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub(crate) struct Parameter<'db> {
/// Annotated type of the parameter.
annotated_ty: Option<Type<'db>>,
annotated_type: Option<Type<'db>>,
kind: ParameterKind<'db>,
pub(crate) form: ParameterForm,
}
impl<'db> Parameter<'db> {
pub(crate) fn new(annotated_ty: Option<Type<'db>>, kind: ParameterKind<'db>) -> Self {
Self { annotated_ty, kind }
pub(crate) fn positional_only(name: Option<Name>) -> Self {
Self {
annotated_type: None,
kind: ParameterKind::PositionalOnly {
name,
default_type: None,
},
form: ParameterForm::Value,
}
}
pub(crate) fn positional_or_keyword(name: Name) -> Self {
Self {
annotated_type: None,
kind: ParameterKind::PositionalOrKeyword {
name,
default_type: None,
},
form: ParameterForm::Value,
}
}
pub(crate) fn variadic(name: Name) -> Self {
Self {
annotated_type: None,
kind: ParameterKind::Variadic { name },
form: ParameterForm::Value,
}
}
pub(crate) fn keyword_only(name: Name) -> Self {
Self {
annotated_type: None,
kind: ParameterKind::KeywordOnly {
name,
default_type: None,
},
form: ParameterForm::Value,
}
}
pub(crate) fn keyword_variadic(name: Name) -> Self {
Self {
annotated_type: None,
kind: ParameterKind::KeywordVariadic { name },
form: ParameterForm::Value,
}
}
pub(crate) fn with_annotated_type(mut self, annotated_type: Type<'db>) -> Self {
self.annotated_type = Some(annotated_type);
self
}
pub(crate) fn with_default_type(mut self, default: Type<'db>) -> Self {
match &mut self.kind {
ParameterKind::PositionalOnly { default_type, .. }
| ParameterKind::PositionalOrKeyword { default_type, .. }
| ParameterKind::KeywordOnly { default_type, .. } => *default_type = Some(default),
ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => {
panic!("cannot set default value for variadic parameter")
}
}
self
}
pub(crate) fn type_form(mut self) -> Self {
self.form = ParameterForm::Type;
self
}
fn from_node_and_kind(
@@ -548,10 +600,11 @@ impl<'db> Parameter<'db> {
kind: ParameterKind<'db>,
) -> Self {
Self {
annotated_ty: parameter
annotated_type: parameter
.annotation()
.map(|annotation| definition_expression_type(db, definition, annotation)),
kind,
form: ParameterForm::Value,
}
}
@@ -598,7 +651,7 @@ impl<'db> Parameter<'db> {
/// Annotated type of the parameter, if annotated.
pub(crate) fn annotated_type(&self) -> Option<Type<'db>> {
self.annotated_ty
self.annotated_type
}
/// Kind of the parameter.
@@ -629,11 +682,10 @@ impl<'db> Parameter<'db> {
/// Default-value type of the parameter, if any.
pub(crate) fn default_type(&self) -> Option<Type<'db>> {
match self.kind {
ParameterKind::PositionalOnly { default_ty, .. } => default_ty,
ParameterKind::PositionalOrKeyword { default_ty, .. } => default_ty,
ParameterKind::Variadic { .. } => None,
ParameterKind::KeywordOnly { default_ty, .. } => default_ty,
ParameterKind::KeywordVariadic { .. } => None,
ParameterKind::PositionalOnly { default_type, .. }
| ParameterKind::PositionalOrKeyword { default_type, .. }
| ParameterKind::KeywordOnly { default_type, .. } => default_type,
ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => None,
}
}
}
@@ -647,14 +699,14 @@ pub(crate) enum ParameterKind<'db> {
/// It is possible for signatures to be defined in ways that leave positional-only parameters
/// nameless (e.g. via `Callable` annotations).
name: Option<Name>,
default_ty: Option<Type<'db>>,
default_type: Option<Type<'db>>,
},
/// Positional-or-keyword parameter, e.g. `def f(x): ...`
PositionalOrKeyword {
/// Parameter name.
name: Name,
default_ty: Option<Type<'db>>,
default_type: Option<Type<'db>>,
},
/// Variadic parameter, e.g. `def f(*args): ...`
@@ -667,7 +719,7 @@ pub(crate) enum ParameterKind<'db> {
KeywordOnly {
/// Parameter name.
name: Name,
default_ty: Option<Type<'db>>,
default_type: Option<Type<'db>>,
},
/// Variadic keywords parameter, e.g. `def f(**kwargs): ...`
@@ -677,6 +729,13 @@ pub(crate) enum ParameterKind<'db> {
},
}
/// Whether a parameter is used as a value or a type form.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) enum ParameterForm {
Value,
Type,
}
#[cfg(test)]
mod tests {
use super::*;
@@ -734,74 +793,28 @@ mod tests {
assert_params(
&sig,
&[
Parameter {
annotated_ty: None,
kind: ParameterKind::PositionalOnly {
name: Some(Name::new_static("a")),
default_ty: None,
},
},
Parameter {
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
kind: ParameterKind::PositionalOnly {
name: Some(Name::new_static("b")),
default_ty: None,
},
},
Parameter {
annotated_ty: None,
kind: ParameterKind::PositionalOnly {
name: Some(Name::new_static("c")),
default_ty: Some(Type::IntLiteral(1)),
},
},
Parameter {
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
kind: ParameterKind::PositionalOnly {
name: Some(Name::new_static("d")),
default_ty: Some(Type::IntLiteral(2)),
},
},
Parameter {
annotated_ty: None,
kind: ParameterKind::PositionalOrKeyword {
name: Name::new_static("e"),
default_ty: Some(Type::IntLiteral(3)),
},
},
Parameter {
annotated_ty: Some(Type::IntLiteral(4)),
kind: ParameterKind::PositionalOrKeyword {
name: Name::new_static("f"),
default_ty: Some(Type::IntLiteral(4)),
},
},
Parameter {
annotated_ty: Some(Type::object(&db)),
kind: ParameterKind::Variadic {
name: Name::new_static("args"),
},
},
Parameter {
annotated_ty: None,
kind: ParameterKind::KeywordOnly {
name: Name::new_static("g"),
default_ty: Some(Type::IntLiteral(5)),
},
},
Parameter {
annotated_ty: Some(Type::IntLiteral(6)),
kind: ParameterKind::KeywordOnly {
name: Name::new_static("h"),
default_ty: Some(Type::IntLiteral(6)),
},
},
Parameter {
annotated_ty: Some(KnownClass::Str.to_instance(&db)),
kind: ParameterKind::KeywordVariadic {
name: Name::new_static("kwargs"),
},
},
Parameter::positional_only(Some(Name::new_static("a"))),
Parameter::positional_only(Some(Name::new_static("b")))
.with_annotated_type(KnownClass::Int.to_instance(&db)),
Parameter::positional_only(Some(Name::new_static("c")))
.with_default_type(Type::IntLiteral(1)),
Parameter::positional_only(Some(Name::new_static("d")))
.with_annotated_type(KnownClass::Int.to_instance(&db))
.with_default_type(Type::IntLiteral(2)),
Parameter::positional_or_keyword(Name::new_static("e"))
.with_default_type(Type::IntLiteral(3)),
Parameter::positional_or_keyword(Name::new_static("f"))
.with_annotated_type(Type::IntLiteral(4))
.with_default_type(Type::IntLiteral(4)),
Parameter::variadic(Name::new_static("args"))
.with_annotated_type(Type::object(&db)),
Parameter::keyword_only(Name::new_static("g"))
.with_default_type(Type::IntLiteral(5)),
Parameter::keyword_only(Name::new_static("h"))
.with_annotated_type(Type::IntLiteral(6))
.with_default_type(Type::IntLiteral(6)),
Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(KnownClass::Str.to_instance(&db)),
],
);
}
@@ -828,15 +841,16 @@ mod tests {
let sig = func.internal_signature(&db);
let [Parameter {
annotated_ty,
annotated_type,
kind: ParameterKind::PositionalOrKeyword { name, .. },
..
}] = &sig.parameters.value[..]
else {
panic!("expected one positional-or-keyword parameter");
};
assert_eq!(name, "a");
// Parameter resolution not deferred; we should see A not B
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "A");
assert_eq!(annotated_type.unwrap().display(&db).to_string(), "A");
}
#[test]
@@ -861,15 +875,16 @@ mod tests {
let sig = func.internal_signature(&db);
let [Parameter {
annotated_ty,
annotated_type,
kind: ParameterKind::PositionalOrKeyword { name, .. },
..
}] = &sig.parameters.value[..]
else {
panic!("expected one positional-or-keyword parameter");
};
assert_eq!(name, "a");
// Parameter resolution deferred; we should see B
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "B");
assert_eq!(annotated_type.unwrap().display(&db).to_string(), "B");
}
#[test]
@@ -894,11 +909,13 @@ mod tests {
let sig = func.internal_signature(&db);
let [Parameter {
annotated_ty: a_annotated_ty,
annotated_type: a_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { name: a_name, .. },
..
}, Parameter {
annotated_ty: b_annotated_ty,
annotated_type: b_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { name: b_name, .. },
..
}] = &sig.parameters.value[..]
else {
panic!("expected two positional-or-keyword parameters");
@@ -935,11 +952,13 @@ mod tests {
let sig = func.internal_signature(&db);
let [Parameter {
annotated_ty: a_annotated_ty,
annotated_type: a_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { name: a_name, .. },
..
}, Parameter {
annotated_ty: b_annotated_ty,
annotated_type: b_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { name: b_name, .. },
..
}] = &sig.parameters.value[..]
else {
panic!("expected two positional-or-keyword parameters");

View File

@@ -83,6 +83,13 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
(
Type::Callable(CallableType::SpecializedGetitem),
Type::Callable(CallableType::SpecializedGetitem),
) => Ordering::Equal,
(Type::Callable(CallableType::SpecializedGetitem), _) => Ordering::Less,
(_, Type::Callable(CallableType::SpecializedGetitem)) => Ordering::Greater,
(Type::Callable(CallableType::General(_)), Type::Callable(CallableType::General(_))) => {
Ordering::Equal
}

View File

@@ -617,8 +617,9 @@ impl<'s> Parser<'s> {
.extension()
.is_none_or(|extension| extension.eq_ignore_ascii_case(expected_extension))
{
let backtick_start = self.line_index(backtick_offsets.0);
bail!(
"File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block"
"File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block at line `{backtick_start}`"
);
}
}
@@ -750,6 +751,10 @@ impl<'s> Parser<'s> {
fn offset(&self) -> TextSize {
self.source_len - self.cursor.text_len()
}
fn line_index(&self, char_index: TextSize) -> u32 {
self.source.count_lines(TextRange::up_to(char_index))
}
}
#[cfg(test)]
@@ -1221,7 +1226,7 @@ mod tests {
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block"
"File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block at line `5`"
);
}

View File

@@ -153,7 +153,7 @@ class MIMEPart(Message[_HeaderRegistryT, _HeaderRegistryParamT]):
def attach(self, payload: Self) -> None: ... # type: ignore[override]
# The attachments are created via type(self) in the attach method. It's theoretically
# possible to sneak other attachment types into a MIMEPart instance, but could cause
# cause unforseen consequences.
# cause unforeseen consequences.
def iter_attachments(self) -> Iterator[Self]: ...
def iter_parts(self) -> Iterator[MIMEPart[_HeaderRegistryT]]: ...
def get_content(self, *args: Any, content_manager: ContentManager | None = None, **kw: Any) -> Any: ...

View File

@@ -20,10 +20,10 @@ default = ["console_error_panic_hook"]
[dependencies]
red_knot_project = { workspace = true, default-features = false, features = ["deflate"] }
red_knot_python_semantic = { workspace = true }
ruff_db = { workspace = true, default-features = false, features = [] }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
@@ -34,6 +34,7 @@ log = { workspace = true }
# Not a direct dependency but required to enable the `wasm_js` feature.
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
getrandom = { workspace = true, features = ["wasm_js"] }
serde-wasm-bindgen = { workspace = true }
wasm-bindgen = { workspace = true }

View File

@@ -1,11 +1,12 @@
use std::any::Any;
use js_sys::{Error, JsString};
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
use red_knot_project::metadata::value::RangedValue;
use red_knot_project::metadata::options::Options;
use red_knot_project::metadata::value::ValueSource;
use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
use red_knot_project::ProjectMetadata;
use red_knot_project::{Db, ProjectDatabase};
use red_knot_python_semantic::Program;
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::source::{line_index, source_text};
@@ -39,62 +40,75 @@ pub fn run() {
pub struct Workspace {
db: ProjectDatabase,
system: WasmSystem,
options: Options,
}
#[wasm_bindgen]
impl Workspace {
#[wasm_bindgen(constructor)]
pub fn new(root: &str, settings: &Settings) -> Result<Workspace, Error> {
pub fn new(root: &str, options: JsValue) -> Result<Workspace, Error> {
let options = Options::deserialize_with(
ValueSource::Cli,
serde_wasm_bindgen::Deserializer::from(options),
)
.map_err(into_error)?;
let system = WasmSystem::new(SystemPath::new(root));
let mut workspace =
ProjectMetadata::discover(SystemPath::new(root), &system).map_err(into_error)?;
let project = ProjectMetadata::from_options(options, SystemPathBuf::from(root), None)
.map_err(into_error)?;
let options = Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(settings.python_version.into())),
..EnvironmentOptions::default()
}),
..Options::default()
};
let db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?;
workspace.apply_cli_options(options.clone());
Ok(Self { db, system })
}
let db = ProjectDatabase::new(workspace, system.clone()).map_err(into_error)?;
#[wasm_bindgen(js_name = "updateOptions")]
pub fn update_options(&mut self, options: JsValue) -> Result<(), Error> {
let options = Options::deserialize_with(
ValueSource::Cli,
serde_wasm_bindgen::Deserializer::from(options),
)
.map_err(into_error)?;
Ok(Self {
db,
system,
let project = ProjectMetadata::from_options(
options,
})
self.db.project().root(&self.db).to_path_buf(),
None,
)
.map_err(into_error)?;
let program_settings = project.to_program_settings(&self.system);
Program::get(&self.db)
.update_from_settings(&mut self.db, program_settings)
.map_err(into_error)?;
self.db.project().reload(&mut self.db, project);
Ok(())
}
#[wasm_bindgen(js_name = "openFile")]
pub fn open_file(&mut self, path: &str, contents: &str) -> Result<FileHandle, Error> {
let path = SystemPath::new(path);
let path = SystemPath::absolute(path, self.db.project().root(&self.db));
self.system
.fs
.write_file_all(path, contents)
.write_file_all(&path, contents)
.map_err(into_error)?;
self.db.apply_changes(
vec![ChangeEvent::Created {
path: path.to_path_buf(),
path: path.clone(),
kind: CreatedKind::File,
}],
Some(&self.options),
None,
);
let file = system_path_to_file(&self.db, path).expect("File to exist");
let file = system_path_to_file(&self.db, &path).expect("File to exist");
self.db.project().open_file(&mut self.db, file);
Ok(FileHandle {
file,
path: path.to_path_buf(),
})
Ok(FileHandle { path, file })
}
#[wasm_bindgen(js_name = "updateFile")]
@@ -119,7 +133,7 @@ impl Workspace {
kind: ChangedKind::FileMetadata,
},
],
Some(&self.options),
None,
);
Ok(())
@@ -140,7 +154,7 @@ impl Workspace {
path: file_id.path.to_path_buf(),
kind: DeletedKind::File,
}],
Some(&self.options),
None,
);
Ok(())
@@ -202,18 +216,6 @@ impl FileHandle {
}
}
#[wasm_bindgen]
pub struct Settings {
pub python_version: PythonVersion,
}
#[wasm_bindgen]
impl Settings {
#[wasm_bindgen(constructor)]
pub fn new(python_version: PythonVersion) -> Self {
Self { python_version }
}
}
#[wasm_bindgen]
pub struct Diagnostic {
#[wasm_bindgen(readonly)]
@@ -332,33 +334,6 @@ impl From<ruff_text_size::TextRange> for TextRange {
}
}
#[wasm_bindgen]
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum PythonVersion {
Py37,
Py38,
#[default]
Py39,
Py310,
Py311,
Py312,
Py313,
}
impl From<PythonVersion> for ruff_python_ast::PythonVersion {
fn from(value: PythonVersion) -> Self {
match value {
PythonVersion::Py37 => Self::PY37,
PythonVersion::Py38 => Self::PY38,
PythonVersion::Py39 => Self::PY39,
PythonVersion::Py310 => Self::PY310,
PythonVersion::Py311 => Self::PY311,
PythonVersion::Py312 => Self::PY312,
PythonVersion::Py313 => Self::PY313,
}
}
}
#[derive(Debug, Clone)]
struct WasmSystem {
fs: MemoryFileSystem,
@@ -455,16 +430,3 @@ impl System for WasmSystem {
fn not_found() -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory")
}
#[cfg(test)]
mod tests {
use crate::PythonVersion;
#[test]
fn same_default_as_python_version() {
assert_eq!(
ruff_python_ast::PythonVersion::from(PythonVersion::default()),
ruff_python_ast::PythonVersion::default()
);
}
}

View File

@@ -1,15 +1,12 @@
#![cfg(target_arch = "wasm32")]
use red_knot_wasm::{Position, Workspace};
use wasm_bindgen_test::wasm_bindgen_test;
use red_knot_wasm::{Position, PythonVersion, Settings, Workspace};
#[wasm_bindgen_test]
fn check() {
let settings = Settings {
python_version: PythonVersion::Py312,
};
let mut workspace = Workspace::new("/", &settings).expect("Workspace to be created");
let mut workspace =
Workspace::new("/", js_sys::JSON::parse("{}").unwrap()).expect("Workspace to be created");
workspace
.open_file("test.py", "import random22\n")

View File

@@ -1054,6 +1054,52 @@ def mvce(keys, values):
"#);
}
#[test]
fn show_statistics_syntax_errors() {
let mut cmd = RuffCheck::default()
.args(["--statistics", "--target-version=py39", "--preview"])
.build();
// ParseError
assert_cmd_snapshot!(
cmd.pass_stdin("x ="),
@r"
success: false
exit_code: 1
----- stdout -----
1 syntax-error
Found 1 error.
----- stderr -----
");
// match before 3.10, UnsupportedSyntaxError
assert_cmd_snapshot!(
cmd.pass_stdin("match 2:\n case 1: ..."),
@r"
success: false
exit_code: 1
----- stdout -----
1 syntax-error
Found 1 error.
----- stderr -----
");
// rebound comprehension variable, SemanticSyntaxError
assert_cmd_snapshot!(
cmd.pass_stdin("[x := 1 for x in range(0)]"),
@r"
success: false
exit_code: 1
----- stdout -----
1 syntax-error
Found 1 error.
----- stderr -----
");
}
#[test]
fn preview_enabled_prefix() {
// All the RUF9XX test rules should be triggered

View File

@@ -5491,3 +5491,68 @@ fn cookiecutter_globbing_no_project_root() -> Result<()> {
Ok(())
}
/// Test that semantic syntax errors (1) are emitted, (2) are not cached, (3) don't affect the
/// reporting of normal diagnostics, and (4) are not suppressed by `select = []` (or otherwise
/// disabling all AST-based rules).
#[test]
fn semantic_syntax_errors() -> Result<()> {
let tempdir = TempDir::new()?;
let contents = "[(x := 1) for x in foo]";
fs::write(tempdir.path().join("main.py"), contents)?;
let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
// inline STDIN_BASE_OPTIONS to remove --no-cache
cmd.args(["check", "--output-format", "concise"])
.arg("--preview")
.arg("--quiet") // suppress `debug build without --no-cache` warnings
.current_dir(&tempdir);
assert_cmd_snapshot!(
cmd,
@r"
success: false
exit_code: 1
----- stdout -----
main.py:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
main.py:1:20: F821 Undefined name `foo`
----- stderr -----
"
);
// this should *not* be cached, like normal parse errors
assert_cmd_snapshot!(
cmd,
@r"
success: false
exit_code: 1
----- stdout -----
main.py:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
main.py:1:20: F821 Undefined name `foo`
----- stderr -----
"
);
// ensure semantic errors are caught even without AST-based rules selected
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "lint.select = []"])
.arg("--preview")
.arg("-")
.pass_stdin(contents),
@r"
success: false
exit_code: 1
----- stdout -----
-:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
Found 1 error.
----- stderr -----
"
);
Ok(())
}

View File

@@ -937,7 +937,7 @@ impl From<snippet::Level> for DisplayAnnotationType {
}
}
/// Information whether the header is the initial one or a consequitive one
/// Information whether the header is the initial one or a consecutive one
/// for multi-slice cases.
// TODO: private
#[derive(Debug, Clone, PartialEq)]

View File

@@ -60,23 +60,13 @@ type KeyDiagnosticFields = (
Severity,
);
static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[
// We don't support `*` imports yet:
(
DiagnosticId::lint("unresolved-import"),
Some("/src/tomllib/_parser.py"),
Some(192..200),
Cow::Borrowed("Module `collections.abc` has no member `Iterable`"),
Severity::Error,
),
(
DiagnosticId::lint("unused-ignore-comment"),
Some("/src/tomllib/_parser.py"),
Some(22299..22333),
Cow::Borrowed("Unused blanket `type: ignore` directive"),
Severity::Warning,
),
];
static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[(
DiagnosticId::lint("unused-ignore-comment"),
Some("/src/tomllib/_parser.py"),
Some(22299..22333),
Cow::Borrowed("Unused blanket `type: ignore` directive"),
Severity::Warning,
)];
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
SystemPathBuf::from("src").join(file.name())

View File

@@ -197,7 +197,7 @@ def test_index_with_attribute():
ds["Foo"] = ds.Foo + 2.0
assert (
ds["Foo"] is ds.Foo
) # This is now modfied, but both methods points to the same object
) # This is now modified, but both methods points to the same object
def test_getitem_time(ds3):

View File

@@ -1,3 +1,5 @@
use crate::system::file_time_now;
/// A number representing the revision of a file.
///
/// Two revisions that don't compare equal signify that the file has been modified.
@@ -16,7 +18,7 @@ impl FileRevision {
}
pub fn now() -> Self {
Self::from(filetime::FileTime::now())
Self::from(file_time_now())
}
pub const fn zero() -> Self {
@@ -53,13 +55,12 @@ impl From<filetime::FileTime> for FileRevision {
#[cfg(test)]
mod tests {
use filetime::FileTime;
use super::*;
#[test]
fn revision_from_file_time() {
let file_time = FileTime::now();
let file_time = file_time_now();
let revision = FileRevision::from(file_time);
let revision = revision.as_u128();

View File

@@ -7,6 +7,7 @@ pub use os::testing::UserConfigDirectoryOverrideGuard;
#[cfg(feature = "os")]
pub use os::OsSystem;
use filetime::FileTime;
use ruff_notebook::{Notebook, NotebookError};
use std::error::Error;
use std::fmt::{Debug, Formatter};
@@ -346,3 +347,27 @@ pub enum GlobErrorKind {
IOError(io::Error),
NonUtf8Path,
}
#[cfg(not(target_arch = "wasm32"))]
pub fn file_time_now() -> FileTime {
FileTime::now()
}
#[cfg(target_arch = "wasm32")]
pub fn file_time_now() -> FileTime {
// Copied from FileTime::from_system_time()
let time = web_time::SystemTime::now();
time.duration_since(web_time::UNIX_EPOCH)
.map(|d| FileTime::from_unix_time(d.as_secs() as i64, d.subsec_nanos()))
.unwrap_or_else(|e| {
let until_epoch = e.duration();
let (sec_offset, nanos) = if until_epoch.subsec_nanos() == 0 {
(0, 0)
} else {
(-1, 1_000_000_000 - until_epoch.subsec_nanos())
};
FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos)
})
}

View File

@@ -7,8 +7,8 @@ use filetime::FileTime;
use rustc_hash::FxHashMap;
use crate::system::{
walk_directory, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result,
SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf,
file_time_now, walk_directory, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata,
Result, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf,
};
use super::walk_directory::{
@@ -163,7 +163,7 @@ impl MemoryFileSystem {
let file = get_or_create_file(&mut by_path, &normalized)?;
file.content = content.to_string();
file.last_modified = now();
file.last_modified = file_time_now();
Ok(())
}
@@ -215,7 +215,7 @@ impl MemoryFileSystem {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(File {
content: content.to_string(),
last_modified: now(),
last_modified: file_time_now(),
});
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
@@ -310,7 +310,7 @@ impl MemoryFileSystem {
let mut by_path = self.inner.by_path.write().unwrap();
let normalized = self.normalize_path(path.as_ref());
get_or_create_file(&mut by_path, &normalized)?.last_modified = now();
get_or_create_file(&mut by_path, &normalized)?.last_modified = file_time_now();
Ok(())
}
@@ -486,7 +486,7 @@ fn create_dir_all(
path.push(component);
let entry = paths.entry(path.clone()).or_insert_with(|| {
Entry::Directory(Directory {
last_modified: now(),
last_modified: file_time_now(),
})
});
@@ -513,7 +513,7 @@ fn get_or_create_file<'a>(
let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
Entry::File(File {
content: String::new(),
last_modified: now(),
last_modified: file_time_now(),
})
});
@@ -695,30 +695,6 @@ enum WalkerState {
Nested { path: SystemPathBuf, depth: usize },
}
#[cfg(not(target_arch = "wasm32"))]
fn now() -> FileTime {
FileTime::now()
}
#[cfg(target_arch = "wasm32")]
fn now() -> FileTime {
// Copied from FileTime::from_system_time()
let time = web_time::SystemTime::now();
time.duration_since(web_time::UNIX_EPOCH)
.map(|d| FileTime::from_unix_time(d.as_secs() as i64, d.subsec_nanos()))
.unwrap_or_else(|e| {
let until_epoch = e.duration();
let (sec_offset, nanos) = if until_epoch.subsec_nanos() == 0 {
(0, 0)
} else {
(-1, 1_000_000_000 - until_epoch.subsec_nanos())
};
FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos)
})
}
#[cfg(test)]
mod tests {
use std::io::ErrorKind;

View File

@@ -894,7 +894,7 @@ struct PrinterState<'a> {
line_suffixes: LineSuffixes<'a>,
verbatim_markers: Vec<TextRange>,
group_modes: GroupModes,
// Re-used queue to measure if a group fits. Optimisation to avoid re-allocating a new
// Reused queue to measure if a group fits. Optimisation to avoid re-allocating a new
// vec every time a group gets measured
fits_stack: Vec<StackFrame>,
fits_queue: Vec<std::slice::Iter<'a, FormatElement>>,

View File

@@ -85,7 +85,7 @@ def decorator_deprecated_operator_args():
bdow_op >> bdow_op2
# deprecated filename_template arugment in FileTaskHandler
# deprecated filename_template argument in FileTaskHandler
S3TaskHandler(filename_template="/tmp/test")
HdfsTaskHandler(filename_template="/tmp/test")
ElasticsearchTaskHandler(filename_template="/tmp/test")

View File

@@ -853,14 +853,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::FutureFeatureNotDefined) {
pyflakes::rules::future_feature_not_defined(checker, alias);
}
if checker.enabled(Rule::LateFutureImport) {
if checker.semantic.seen_futures_boundary() {
checker.report_diagnostic(Diagnostic::new(
pyflakes::rules::LateFutureImport,
stmt.range(),
));
}
}
} else if &alias.name == "*" {
if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {

View File

@@ -26,6 +26,9 @@ use std::path::Path;
use itertools::Itertools;
use log::debug;
use ruff_python_parser::semantic_errors::{
SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind,
};
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_diagnostics::{Diagnostic, IsolationLevel};
@@ -63,6 +66,7 @@ use crate::importer::Importer;
use crate::noqa::NoqaMapping;
use crate::package::PackageRoot;
use crate::registry::Rule;
use crate::rules::pyflakes::rules::LateFutureImport;
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
use crate::settings::{flags, LinterSettings};
use crate::{docstrings, noqa, Locator};
@@ -223,8 +227,13 @@ pub(crate) struct Checker<'a> {
last_stmt_end: TextSize,
/// A state describing if a docstring is expected or not.
docstring_state: DocstringState,
/// The target [`PythonVersion`] for version-dependent checks
/// The target [`PythonVersion`] for version-dependent checks.
target_version: PythonVersion,
/// Helper visitor for detecting semantic syntax errors.
#[allow(clippy::struct_field_names)]
semantic_checker: SemanticSyntaxChecker,
/// Errors collected by the `semantic_checker`.
semantic_errors: RefCell<Vec<SemanticSyntaxError>>,
}
impl<'a> Checker<'a> {
@@ -272,6 +281,8 @@ impl<'a> Checker<'a> {
last_stmt_end: TextSize::default(),
docstring_state: DocstringState::default(),
target_version,
semantic_checker: SemanticSyntaxChecker::new(),
semantic_errors: RefCell::default(),
}
}
}
@@ -512,10 +523,50 @@ impl<'a> Checker<'a> {
pub(crate) const fn target_version(&self) -> PythonVersion {
self.target_version
}
fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Checker)) {
let mut checker = std::mem::take(&mut self.semantic_checker);
f(&mut checker, self);
self.semantic_checker = checker;
}
}
impl SemanticSyntaxContext for Checker<'_> {
fn seen_docstring_boundary(&self) -> bool {
self.semantic.seen_module_docstring_boundary()
}
fn python_version(&self) -> PythonVersion {
self.target_version
}
fn report_semantic_error(&self, error: SemanticSyntaxError) {
match error.kind {
SemanticSyntaxErrorKind::LateFutureImport => {
if self.settings.rules.enabled(Rule::LateFutureImport) {
self.report_diagnostic(Diagnostic::new(LateFutureImport, error.range));
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_)
if self.settings.preview.is_enabled() =>
{
self.semantic_errors.borrow_mut().push(error);
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_) => {}
}
}
}
impl<'a> Visitor<'a> for Checker<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context));
// Step 0: Pre-processing
self.semantic.push_node(stmt);
@@ -550,17 +601,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
{
self.semantic.flags |= SemanticModelFlags::FUTURE_ANNOTATIONS;
}
} else {
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
}
}
Stmt::Import(_) => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
}
_ => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !(self.semantic.seen_import_boundary()
|| stmt.is_ipy_escape_command_stmt()
|| helpers::is_assignment_to_a_dunder(stmt)
@@ -1131,6 +1178,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
fn visit_expr(&mut self, expr: &'a Expr) {
self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context));
// Step 0: Pre-processing
if self.source_type.is_stub()
&& self.semantic.in_class_base()
@@ -2674,7 +2723,7 @@ pub(crate) fn check_ast(
cell_offsets: Option<&CellOffsets>,
notebook_index: Option<&NotebookIndex>,
target_version: PythonVersion,
) -> Vec<Diagnostic> {
) -> (Vec<Diagnostic>, Vec<SemanticSyntaxError>) {
let module_path = package
.map(PackageRoot::path)
.and_then(|package| to_module_path(package, path));
@@ -2739,5 +2788,11 @@ pub(crate) fn check_ast(
checker.analyze.scopes.push(ScopeId::global());
analyze::deferred_scopes(&checker);
checker.diagnostics.take()
let Checker {
diagnostics,
semantic_errors,
..
} = checker;
(diagnostics.into_inner(), semantic_errors.into_inner())
}

View File

@@ -6,6 +6,7 @@ use std::path::Path;
use anyhow::{anyhow, Result};
use colored::Colorize;
use itertools::Itertools;
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@@ -43,9 +44,9 @@ pub struct LinterResult {
/// Flag indicating that the parsed source code does not contain any
/// [`ParseError`]s
has_valid_syntax: bool,
/// Flag indicating that the parsed source code does not contain any
/// [`UnsupportedSyntaxError`]s
has_no_unsupported_syntax_errors: bool,
/// Flag indicating that the parsed source code does not contain any [`ParseError`]s,
/// [`UnsupportedSyntaxError`]s, or [`SemanticSyntaxError`]s.
has_no_syntax_errors: bool,
}
impl LinterResult {
@@ -62,7 +63,7 @@ impl LinterResult {
///
/// See [`LinterResult::has_valid_syntax`] for a version specific to [`ParseError`]s.
pub fn has_no_syntax_errors(&self) -> bool {
self.has_valid_syntax() && self.has_no_unsupported_syntax_errors
self.has_valid_syntax() && self.has_no_syntax_errors
}
/// Returns `true` if the parsed source code is valid i.e., it has no [`ParseError`]s.
@@ -114,6 +115,9 @@ pub fn check_path(
// Aggregate all diagnostics.
let mut diagnostics = vec![];
// Aggregate all semantic syntax errors.
let mut semantic_syntax_errors = vec![];
let tokens = parsed.tokens();
let comment_ranges = indexer.comment_ranges();
@@ -172,35 +176,33 @@ pub fn check_path(
// Run the AST-based rules only if there are no syntax errors.
if parsed.has_valid_syntax() {
let use_ast = settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_ast());
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
let (new_diagnostics, new_semantic_syntax_errors) = check_ast(
parsed,
locator,
stylist,
indexer,
&directives.noqa_line_for,
settings,
noqa,
path,
package,
source_type,
cell_offsets,
notebook_index,
target_version,
);
diagnostics.extend(new_diagnostics);
semantic_syntax_errors.extend(new_semantic_syntax_errors);
let use_imports = !directives.isort.skip_file
&& settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
if use_ast {
diagnostics.extend(check_ast(
parsed,
locator,
stylist,
indexer,
&directives.noqa_line_for,
settings,
noqa,
path,
package,
source_type,
cell_offsets,
notebook_index,
target_version,
));
}
if use_imports || use_doc_lines {
if use_imports {
let import_diagnostics = check_imports(
parsed,
@@ -368,6 +370,7 @@ pub fn check_path(
diagnostics,
parsed.errors(),
syntax_errors,
&semantic_syntax_errors,
path,
locator,
directives,
@@ -482,9 +485,9 @@ pub fn lint_only(
);
LinterResult {
messages,
has_valid_syntax: parsed.has_valid_syntax(),
has_no_unsupported_syntax_errors: parsed.unsupported_syntax_errors().is_empty(),
has_no_syntax_errors: !messages.iter().any(Message::is_syntax_error),
messages,
}
}
@@ -493,6 +496,7 @@ fn diagnostics_to_messages(
diagnostics: Vec<Diagnostic>,
parse_errors: &[ParseError],
unsupported_syntax_errors: &[UnsupportedSyntaxError],
semantic_syntax_errors: &[SemanticSyntaxError],
path: &Path,
locator: &Locator,
directives: &Directives,
@@ -514,6 +518,11 @@ fn diagnostics_to_messages(
.chain(unsupported_syntax_errors.iter().map(|syntax_error| {
Message::from_unsupported_syntax_error(syntax_error, file.deref().clone())
}))
.chain(
semantic_syntax_errors
.iter()
.map(|error| Message::from_semantic_syntax_error(error, file.deref().clone())),
)
.chain(diagnostics.into_iter().map(|diagnostic| {
let noqa_offset = directives.noqa_line_for.resolve(diagnostic.start());
Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_offset)
@@ -545,7 +554,7 @@ pub fn lint_fix<'a>(
let mut has_valid_syntax = false;
// Track whether the _initial_ source code has no unsupported syntax errors.
let mut has_no_unsupported_syntax_errors = false;
let mut has_no_syntax_errors = false;
let target_version = settings.resolve_target_version(path);
@@ -589,12 +598,12 @@ pub fn lint_fix<'a>(
if iterations == 0 {
has_valid_syntax = parsed.has_valid_syntax();
has_no_unsupported_syntax_errors = parsed.unsupported_syntax_errors().is_empty();
has_no_syntax_errors = !messages.iter().any(Message::is_syntax_error);
} else {
// If the source code had no syntax errors on the first pass, but
// does on a subsequent pass, then we've introduced a
// syntax error. Return the original code.
if has_valid_syntax && has_no_unsupported_syntax_errors {
if has_valid_syntax && has_no_syntax_errors {
if let Some(error) = parsed.errors().first() {
report_fix_syntax_error(
path,
@@ -636,7 +645,7 @@ pub fn lint_fix<'a>(
result: LinterResult {
messages,
has_valid_syntax,
has_no_unsupported_syntax_errors,
has_no_syntax_errors,
},
transformed,
fixed,

View File

@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use std::io::Write;
use std::ops::Deref;
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
use rustc_hash::FxHashMap;
pub use azure::AzureEmitter;
@@ -135,6 +136,18 @@ impl Message {
})
}
/// Create a [`Message`] from the given [`SemanticSyntaxError`].
pub fn from_semantic_syntax_error(
semantic_syntax_error: &SemanticSyntaxError,
file: SourceFile,
) -> Message {
Message::SyntaxError(SyntaxErrorMessage {
message: format!("SyntaxError: {semantic_syntax_error}"),
range: semantic_syntax_error.range,
file,
})
}
pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> {
match self {
Message::Diagnostic(m) => Some(m),

View File

@@ -241,21 +241,9 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
Some("logical_date"),
));
}
["airflow", .., "operators", "datetime", "BranchDateTimeOperator"] => {
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
["airflow", .., "operators", "weekday", "DayOfWeekSensor"] => {
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
["airflow", .., "operators", "weekday", "BranchDayOfWeekOperator"] => {
["airflow", .., "operators", "datetime", "BranchDateTimeOperator"]
| ["airflow", .., "operators", "weekday", "DayOfWeekSensor" | "BranchDayOfWeekOperator"] =>
{
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"use_task_execution_day",
@@ -288,29 +276,27 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) {
let replacement = match *qualname.segments() {
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
"dataset_factories" => Some(Replacement::Name("asset_factories")),
"dataset_uri_handlers" => Some(Replacement::Name("asset_uri_handlers")),
"dataset_factories" => Replacement::Name("asset_factories"),
"dataset_uri_handlers" => Replacement::Name("asset_uri_handlers"),
"dataset_to_openlineage_converters" => {
Some(Replacement::Name("asset_to_openlineage_converters"))
Replacement::Name("asset_to_openlineage_converters")
}
_ => None,
_ => return,
},
["airflow", "lineage", "hook", "DatasetLineageInfo"] => match attr.as_str() {
"dataset" => Some(Replacement::Name("asset")),
_ => None,
"dataset" => Replacement::Name("asset"),
_ => return,
},
_ => None,
_ => return,
};
if let Some(replacement) = replacement {
checker.report_diagnostic(Diagnostic::new(
Airflow3Removal {
deprecated: attr.to_string(),
replacement,
},
attr.range(),
));
}
checker.report_diagnostic(Diagnostic::new(
Airflow3Removal {
deprecated: attr.to_string(),
replacement,
},
attr.range(),
));
}
/// Checks whether an Airflow 3.0removed context key is used in a function decorated with `@task`.
@@ -465,68 +451,66 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
let replacement = match qualname.segments() {
["airflow", "datasets", "manager", "DatasetManager"] => match attr.as_str() {
"register_dataset_change" => Some(Replacement::Name("register_asset_change")),
"create_datasets" => Some(Replacement::Name("create_assets")),
"notify_dataset_created" => Some(Replacement::Name("notify_asset_created")),
"notify_dataset_changed" => Some(Replacement::Name("notify_asset_changed")),
"notify_dataset_alias_created" => Some(Replacement::Name("notify_asset_alias_created")),
_ => None,
"register_dataset_change" => Replacement::Name("register_asset_change"),
"create_datasets" => Replacement::Name("create_assets"),
"notify_dataset_created" => Replacement::Name("notify_asset_created"),
"notify_dataset_changed" => Replacement::Name("notify_asset_changed"),
"notify_dataset_alias_created" => Replacement::Name("notify_asset_alias_created"),
_ => return,
},
["airflow", "lineage", "hook", "HookLineageCollector"] => match attr.as_str() {
"create_dataset" => Some(Replacement::Name("create_asset")),
"add_input_dataset" => Some(Replacement::Name("add_input_asset")),
"add_output_dataset" => Some(Replacement::Name("add_output_asset")),
"collected_datasets" => Some(Replacement::Name("collected_assets")),
_ => None,
"create_dataset" => Replacement::Name("create_asset"),
"add_input_dataset" => Replacement::Name("add_input_asset"),
"add_output_dataset" => Replacement::Name("add_output_asset"),
"collected_datasets" => Replacement::Name("collected_assets"),
_ => return,
},
["airflow", "providers", "amazon", "auth_manager", "aws_auth_manager", "AwsAuthManager"] => {
match attr.as_str() {
"is_authorized_dataset" => Some(Replacement::Name("is_authorized_asset")),
_ => None,
"is_authorized_dataset" => Replacement::Name("is_authorized_asset"),
_ => return,
}
}
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
"initialize_providers_dataset_uri_resources" => Some(Replacement::Name(
"initialize_providers_asset_uri_resources",
)),
_ => None,
"initialize_providers_dataset_uri_resources" => {
Replacement::Name("initialize_providers_asset_uri_resources")
}
_ => return,
},
["airflow", "secrets", "local_filesystem", "LocalFilesystemBackend"] => match attr.as_str()
{
"get_connections" => Some(Replacement::Name("get_connection")),
_ => None,
"get_connections" => Replacement::Name("get_connection"),
_ => return,
},
["airflow", "datasets", ..] | ["airflow", "Dataset"] => match attr.as_str() {
"iter_datasets" => Some(Replacement::Name("iter_assets")),
"iter_dataset_aliases" => Some(Replacement::Name("iter_asset_aliases")),
_ => None,
"iter_datasets" => Replacement::Name("iter_assets"),
"iter_dataset_aliases" => Replacement::Name("iter_asset_aliases"),
_ => return,
},
segments => {
if is_airflow_secret_backend(segments) {
match attr.as_str() {
"get_conn_uri" => Some(Replacement::Name("get_conn_value")),
"get_connections" => Some(Replacement::Name("get_connection")),
_ => None,
"get_conn_uri" => Replacement::Name("get_conn_value"),
"get_connections" => Replacement::Name("get_connection"),
_ => return,
}
} else if is_airflow_hook(segments) {
match attr.as_str() {
"get_connections" => Some(Replacement::Name("get_connection")),
_ => None,
"get_connections" => Replacement::Name("get_connection"),
_ => return,
}
} else {
None
return;
}
}
};
if let Some(replacement) = replacement {
checker.report_diagnostic(Diagnostic::new(
Airflow3Removal {
deprecated: attr.to_string(),
replacement,
},
attr.range(),
));
}
checker.report_diagnostic(Diagnostic::new(
Airflow3Removal {
deprecated: attr.to_string(),
replacement,
},
attr.range(),
));
}
/// Check whether a removed Airflow name is used.
@@ -613,25 +597,18 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
}
// airflow.datasets
["airflow", "Dataset"] => Replacement::Name("airflow.sdk.definitions.asset.Asset"),
["airflow", "Dataset"] | ["airflow", "datasets", "Dataset"] => {
Replacement::Name("airflow.sdk.Asset")
}
["airflow", "datasets", "DatasetAliasEvent"] => Replacement::None,
["airflow", "datasets", "Dataset"] => {
Replacement::Name("airflow.sdk.definitions.asset.Asset")
}
["airflow", "datasets", "DatasetAlias"] => {
Replacement::Name("airflow.sdk.definitions.asset.AssetAlias")
}
["airflow", "datasets", "DatasetAll"] => {
Replacement::Name("airflow.sdk.definitions.asset.AssetAll")
}
["airflow", "datasets", "DatasetAny"] => {
Replacement::Name("airflow.sdk.definitions.asset.AssetAny")
}
["airflow", "datasets", "DatasetAlias"] => Replacement::Name("airflow.sdk.AssetAlias"),
["airflow", "datasets", "DatasetAll"] => Replacement::Name("airflow.sdk.AssetAll"),
["airflow", "datasets", "DatasetAny"] => Replacement::Name("airflow.sdk.AssetAny"),
["airflow", "datasets", "expand_alias_to_datasets"] => {
Replacement::Name("airflow.sdk.definitions.asset.expand_alias_to_assets")
Replacement::Name("airflow.sdk.expand_alias_to_assets")
}
["airflow", "datasets", "metadata", "Metadata"] => {
Replacement::Name("airflow.sdk.definitions.asset.metadata.Metadata")
Replacement::Name("airflow.sdk.Metadata")
}
// airflow.datasets.manager

View File

@@ -228,7 +228,7 @@ AIR302_args.py:67:9: AIR302 `sla` is removed in Airflow 3.0
AIR302_args.py:89:15: AIR302 `filename_template` is removed in Airflow 3.0
|
88 | # deprecated filename_template arugment in FileTaskHandler
88 | # deprecated filename_template argument in FileTaskHandler
89 | S3TaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^ AIR302
90 | HdfsTaskHandler(filename_template="/tmp/test")
@@ -237,7 +237,7 @@ AIR302_args.py:89:15: AIR302 `filename_template` is removed in Airflow 3.0
AIR302_args.py:90:17: AIR302 `filename_template` is removed in Airflow 3.0
|
88 | # deprecated filename_template arugment in FileTaskHandler
88 | # deprecated filename_template argument in FileTaskHandler
89 | S3TaskHandler(filename_template="/tmp/test")
90 | HdfsTaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^ AIR302

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
snapshot_kind: text
---
AIR302_class_attribute.py:24:21: AIR302 `airflow.Dataset` is removed in Airflow 3.0
|
@@ -10,7 +9,7 @@ AIR302_class_attribute.py:24:21: AIR302 `airflow.Dataset` is removed in Airflow
25 | dataset_from_root.iter_datasets()
26 | dataset_from_root.iter_dataset_aliases()
|
= help: Use `airflow.sdk.definitions.asset.Asset` instead
= help: Use `airflow.sdk.Asset` instead
AIR302_class_attribute.py:25:19: AIR302 `iter_datasets` is removed in Airflow 3.0
|
@@ -41,7 +40,7 @@ AIR302_class_attribute.py:29:31: AIR302 `airflow.datasets.Dataset` is removed in
30 | dataset_to_test_method_call.iter_datasets()
31 | dataset_to_test_method_call.iter_dataset_aliases()
|
= help: Use `airflow.sdk.definitions.asset.Asset` instead
= help: Use `airflow.sdk.Asset` instead
AIR302_class_attribute.py:30:29: AIR302 `iter_datasets` is removed in Airflow 3.0
|
@@ -73,7 +72,7 @@ AIR302_class_attribute.py:33:29: AIR302 `airflow.datasets.DatasetAlias` is remov
34 | alias_to_test_method_call.iter_datasets()
35 | alias_to_test_method_call.iter_dataset_aliases()
|
= help: Use `airflow.sdk.definitions.asset.AssetAlias` instead
= help: Use `airflow.sdk.AssetAlias` instead
AIR302_class_attribute.py:34:27: AIR302 `iter_datasets` is removed in Airflow 3.0
|
@@ -104,7 +103,7 @@ AIR302_class_attribute.py:37:27: AIR302 `airflow.datasets.DatasetAny` is removed
38 | any_to_test_method_call.iter_datasets()
39 | any_to_test_method_call.iter_dataset_aliases()
|
= help: Use `airflow.sdk.definitions.asset.AssetAny` instead
= help: Use `airflow.sdk.AssetAny` instead
AIR302_class_attribute.py:38:25: AIR302 `iter_datasets` is removed in Airflow 3.0
|

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
assertion_line: 29
---
AIR302_names.py:107:1: AIR302 `airflow.PY36` is removed in Airflow 3.0
|
@@ -74,7 +73,7 @@ AIR302_names.py:108:1: AIR302 `airflow.Dataset` is removed in Airflow 3.0
109 |
110 | # airflow.api_connexion.security
|
= help: Use `airflow.sdk.definitions.asset.Asset` instead
= help: Use `airflow.sdk.Asset` instead
AIR302_names.py:111:1: AIR302 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0
|
@@ -197,7 +196,7 @@ AIR302_names.py:125:1: AIR302 `airflow.datasets.Dataset` is removed in Airflow 3
126 | DatasetAlias()
127 | DatasetAliasEvent()
|
= help: Use `airflow.sdk.definitions.asset.Asset` instead
= help: Use `airflow.sdk.Asset` instead
AIR302_names.py:126:1: AIR302 `airflow.datasets.DatasetAlias` is removed in Airflow 3.0
|
@@ -208,7 +207,7 @@ AIR302_names.py:126:1: AIR302 `airflow.datasets.DatasetAlias` is removed in Airf
127 | DatasetAliasEvent()
128 | DatasetAll()
|
= help: Use `airflow.sdk.definitions.asset.AssetAlias` instead
= help: Use `airflow.sdk.AssetAlias` instead
AIR302_names.py:127:1: AIR302 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
|
@@ -229,7 +228,7 @@ AIR302_names.py:128:1: AIR302 `airflow.datasets.DatasetAll` is removed in Airflo
129 | DatasetAny()
130 | Metadata()
|
= help: Use `airflow.sdk.definitions.asset.AssetAll` instead
= help: Use `airflow.sdk.AssetAll` instead
AIR302_names.py:129:1: AIR302 `airflow.datasets.DatasetAny` is removed in Airflow 3.0
|
@@ -240,7 +239,7 @@ AIR302_names.py:129:1: AIR302 `airflow.datasets.DatasetAny` is removed in Airflo
130 | Metadata()
131 | expand_alias_to_datasets
|
= help: Use `airflow.sdk.definitions.asset.AssetAny` instead
= help: Use `airflow.sdk.AssetAny` instead
AIR302_names.py:130:1: AIR302 `airflow.datasets.metadata.Metadata` is removed in Airflow 3.0
|
@@ -250,7 +249,7 @@ AIR302_names.py:130:1: AIR302 `airflow.datasets.metadata.Metadata` is removed in
| ^^^^^^^^ AIR302
131 | expand_alias_to_datasets
|
= help: Use `airflow.sdk.definitions.asset.metadata.Metadata` instead
= help: Use `airflow.sdk.Metadata` instead
AIR302_names.py:131:1: AIR302 `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0
|
@@ -261,7 +260,7 @@ AIR302_names.py:131:1: AIR302 `airflow.datasets.expand_alias_to_datasets` is rem
132 |
133 | # airflow.datasets.manager
|
= help: Use `airflow.sdk.definitions.asset.expand_alias_to_assets` instead
= help: Use `airflow.sdk.expand_alias_to_assets` instead
AIR302_names.py:135:1: AIR302 `airflow.datasets.manager.dataset_manager` is removed in Airflow 3.0
|

View File

@@ -3124,7 +3124,7 @@ lambda: fu
#[test]
fn redefined_by_gen_exp() {
// Re-using a global name as the loop variable for a generator
// Reusing a global name as the loop variable for a generator
// expression results in a redefinition warning.
flakes(
"import fu; (1 for fu in range(1))",

View File

@@ -31,6 +31,7 @@ use ruff_text_size::Ranged;
/// Use instead:
/// ```python
/// fruits = ["orange", "apple"]
/// vegetables = []
///
/// if fruits:
/// print(fruits)

View File

@@ -9,21 +9,37 @@ use crate::{checkers::ast::Checker, importer::ImportRequest};
/// Checks for subclasses of `dict`, `list` or `str`.
///
/// ## Why is this bad?
/// Subclassing `dict`, `list`, or `str` objects can be error prone, use the
/// `UserDict`, `UserList`, and `UserString` objects from the `collections` module
/// Built-in types don't consistently use their own dunder methods. For example,
/// `dict.__init__` and `dict.update()` bypass `__setitem__`, making inheritance unreliable.
///
/// Use the `UserDict`, `UserList`, and `UserString` objects from the `collections` module
/// instead.
///
/// ## Example
///
/// ```python
/// class CaseInsensitiveDict(dict): ...
/// class UppercaseDict(dict):
/// def __setitem__(self, key, value):
/// super().__setitem__(key.upper(), value)
///
///
/// d = UppercaseDict({"a": 1, "b": 2}) # Bypasses __setitem__
/// print(d) # {'a': 1, 'b': 2}
/// ```
///
/// Use instead:
///
/// ```python
/// from collections import UserDict
///
///
/// class CaseInsensitiveDict(UserDict): ...
/// class UppercaseDict(UserDict):
/// def __setitem__(self, key, value):
/// super().__setitem__(key.upper(), value)
///
///
/// d = UppercaseDict({"a": 1, "b": 2}) # Uses __setitem__
/// print(d) # {'A': 1, 'B': 2}
/// ```
///
/// ## Fix safety

View File

@@ -104,7 +104,7 @@ fields = [{ name = "targets", type = "Expr*" }]
doc = "See also [TypeAlias](https://docs.python.org/3/library/ast.html#ast.TypeAlias)"
fields = [
{ name = "name", type = "Expr" },
{ name = "type_params", type = "TypeParams?" },
{ name = "type_params", type = "Box<crate::TypeParams>?" },
{ name = "value", type = "Expr" },
]

View File

@@ -6494,7 +6494,7 @@ pub struct StmtDelete {
pub struct StmtTypeAlias {
pub range: ruff_text_size::TextRange,
pub name: Box<Expr>,
pub type_params: Option<crate::TypeParams>,
pub type_params: Option<Box<crate::TypeParams>>,
pub value: Box<Expr>,
}

View File

@@ -1622,10 +1622,10 @@ mod tests {
});
let type_alias = Stmt::TypeAlias(StmtTypeAlias {
name: Box::new(name.clone()),
type_params: Some(TypeParams {
type_params: Some(Box::new(TypeParams {
type_params: vec![type_var_one, type_var_two],
range: TextRange::default(),
}),
})),
value: Box::new(constant_three.clone()),
range: TextRange::default(),
});

View File

@@ -2244,12 +2244,33 @@ impl Pattern {
///
/// [irrefutable pattern]: https://peps.python.org/pep-0634/#irrefutable-case-blocks
pub fn is_irrefutable(&self) -> bool {
self.irrefutable_pattern().is_some()
}
/// Return `Some(IrrefutablePattern)` if `self` is irrefutable or `None` otherwise.
pub fn irrefutable_pattern(&self) -> Option<IrrefutablePattern> {
match self {
Pattern::MatchAs(PatternMatchAs { pattern: None, .. }) => true,
Pattern::MatchAs(PatternMatchAs {
pattern,
name,
range,
}) => match pattern {
Some(pattern) => pattern.irrefutable_pattern(),
None => match name {
Some(name) => Some(IrrefutablePattern {
kind: IrrefutablePatternKind::Name(name.id.clone()),
range: *range,
}),
None => Some(IrrefutablePattern {
kind: IrrefutablePatternKind::Wildcard,
range: *range,
}),
},
},
Pattern::MatchOr(PatternMatchOr { patterns, .. }) => {
patterns.iter().any(Pattern::is_irrefutable)
patterns.iter().find_map(Pattern::irrefutable_pattern)
}
_ => false,
_ => None,
}
}
@@ -2277,6 +2298,17 @@ impl Pattern {
}
}
pub struct IrrefutablePattern {
pub kind: IrrefutablePatternKind,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IrrefutablePatternKind {
Name(Name),
Wildcard,
}
/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)
#[derive(Clone, Debug, PartialEq)]
pub struct PatternMatchValue {

View File

@@ -0,0 +1,7 @@
type Alias[T, T] = ...
def f[T, T](t: T): ...
class C[T, T]: ...
type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ...
def f[T, T, T](): ... # two errors
def f[T, *T](): ... # star is still duplicate
def f[T, **T](): ... # as is double star

View File

@@ -0,0 +1,12 @@
match x:
case var: ... # capture pattern
case 2: ...
match x:
case _: ...
case 2: ... # wildcard pattern
match x:
case var1 as var2: ... # as pattern with irrefutable left-hand side
case 2: ...
match x:
case enum.variant | var: ... # or pattern with irrefutable part
case 2: ...

View File

@@ -0,0 +1,10 @@
match 2:
case [y, z, y]: ... # MatchSequence
case [y, z, *y]: ... # MatchSequence
case [y, y, y]: ... # MatchSequence multiple
case {1: x, 2: x}: ... # MatchMapping duplicate pattern
case {1: x, **x}: ... # MatchMapping duplicate in **rest
case Class(x, x): ... # MatchClass positional
case Class(x=1, x=2): ... # MatchClass keyword
case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr
case x as x: ... # MatchAs

View File

@@ -0,0 +1,9 @@
[(a := 0) for a in range(0)]
{(a := 0) for a in range(0)}
{(a := 0): val for a in range(0)}
{key: (a := 0) for a in range(0)}
((a := 0) for a in range(0))
[[(a := 0)] for a in range(0)]
[(a := 0) for b in range (0) for a in range(0)]
[(a := 0) for a in range (0) for b in range(0)]
[((a := 0), (b := 1)) for a in range (0) for b in range(0)]

View File

@@ -0,0 +1,9 @@
match x:
case 2: ...
case var: ...
match x:
case 2: ...
case _: ...
match x:
case var if True: ... # don't try to refute a guarded pattern
case 2: ...

View File

@@ -1,3 +1,4 @@
match foo:
case foo_bar: ...
match foo:
case _: ...

View File

@@ -1,4 +1,6 @@
match foo:
case case: ...
match foo:
case match: ...
match foo:
case type: ...

View File

@@ -1,5 +1,5 @@
match subject:
case a: ...
case a if x: ...
case a, b: ...
case a, b if x: ...
case a: ...

View File

@@ -0,0 +1,2 @@
match 2:
case Class(x) | [x] | x: ...

View File

@@ -0,0 +1,5 @@
type Alias[T] = list[T]
def f[T](t: T): ...
class C[T]: ...
class C[T, U, V]: ...
type Alias[T, U: str, V: (str, bytes), *Ts, **P, D = default] = ...

View File

@@ -0,0 +1 @@
[a := 0 for x in range(0)]

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