Compare commits

...

28 Commits

Author SHA1 Message Date
David Peter
71e07fbae3 [red-knot] mypy_primer: split installation and execution 2025-03-11 12:59:14 +01:00
David Peter
0af4985067 [red-knot] mypy_primer: pipeline improvements (#16620)
## Summary

- Add comment to explain `sed` command
- Fix double reporting of diff
- Hide (large) diffs in `<details>`
2025-03-11 11:13:33 +01:00
Dhruv Manilawala
da069aa00c [red-knot] Infer lambda expression (#16547)
## Summary

Part of https://github.com/astral-sh/ruff/issues/15382

This PR adds support for inferring the `lambda` expression and return
the `CallableType`.

Currently, this is only limited to inferring the parameters and a todo
type for the return type.

For posterity, I tried using the `file_expression_type` to infer the
return type of lambda but it would always lead to cycle. The main reason
is that in `infer_parameter_definition`, the default expression is being
inferred using `file_expression_type`, which is correct, but it then

Take the following source code as an example:
```py
lambda x=1: x
```

Here's how the code will flow:
* `infer_scope_types` for the global scope
* `infer_lambda_expression`
* `infer_expression` for the default value `1`
* `file_expression_type` for the return type using the body expression.
This is because the body creates it's own scope
* `infer_scope_types` (lambda body scope)
* `infer_name_load` for the symbol `x` whose visible binding is the
lambda parameter `x`
* `infer_parameter_definition` for parameter `x`
* `file_expression_type` for the default value `1`
* `infer_scope_types` for the global scope because of the default
expression

This will then reach to `infer_definition` for the parameter `x` again
which then creates the cycle.

## Test Plan

Add tests around `lambda` expression inference.
2025-03-11 11:25:20 +05:30
David Peter
ec9ee93d68 [red-knot] mypy_primer: strip ANSI codes (#16604)
## Summary

Strip ANSI codes in the mypy_primer diff before uploading.

## Test Plan

Successful run here: https://github.com/astral-sh/ruff/pull/16601
2025-03-10 17:32:01 +01:00
David Peter
a73548d0ca [red-knot] mypy_primer: comment on PRs (#16599)
## Summary

Add a new pipeline to comment on PRs if there is a mypy_primer diff
result.

## Test Plan

Not yet, I'm afraid I will have to merge this first to have the pipeline
available on main.
2025-03-10 15:25:42 +01:00
David Peter
c60e8a037a [red-knot] Add support for calling type[…] (#16597)
## Summary

This fixes the non-diagnostics part of #15948.

## Test Plan

New Markdown tests.

Negative diff on the ecosystem checks:

```diff
zipp (https://github.com/jaraco/zipp)
- error: lint:call-non-callable
-    --> /tmp/mypy_primer/projects/zipp/zipp/__init__.py:393:16
-     |
- 392 |     def _next(self, at):
- 393 |         return self.__class__(self.root, at)
-     |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `type[Unknown]` is not callable
- 394 |
- 395 |     def is_dir(self):
-     |
- 
- Found 9 diagnostics
+ Found 8 diagnostics

arrow (https://github.com/arrow-py/arrow)
+     |
+     |
+ warning: lint:unused-ignore-comment
+    --> /tmp/mypy_primer/projects/arrow/arrow/arrow.py:576:66
+ 574 |                 values.append(1)
+ 575 |
+ 576 |             floor = self.__class__(*values, tzinfo=self.tzinfo)  # type: ignore[misc]
+     |                                                                  -------------------- Unused blanket `type: ignore` directive
+ 577 |
+ 578 |             if frame_absolute == "week":
- error: lint:call-non-callable
-     --> /tmp/mypy_primer/projects/arrow/arrow/arrow.py:1080:16
-      |
- 1078 |           dt = self._datetime.astimezone(tz)
- 1079 |
- 1080 |           return self.__class__(
-      |  ________________^
- 1081 | |             dt.year,
- 1082 | |             dt.month,
- 1083 | |             dt.day,
- 1084 | |             dt.hour,
- 1085 | |             dt.minute,
- 1086 | |             dt.second,
- 1087 | |             dt.microsecond,
- 1088 | |             dt.tzinfo,
- 1089 | |             fold=getattr(dt, "fold", 0),
- 1090 | |         )
-      | |_________^ Object of type `type[Unknown]` is not callable
- 1091 |
- 1092 |       # string output and formatting
-      |

black (https://github.com/psf/black)
- 
-     |
-     |
- error: lint:call-non-callable
-    --> /tmp/mypy_primer/projects/black/src/blib2to3/pgen2/grammar.py:135:15
- 133 |         Copy the grammar.
- 134 |         """
- 135 |         new = self.__class__()
-     |               ^^^^^^^^^^^^^^^^ Object of type `type[@Todo]` is not callable
- 136 |         for dict_attr in (
- 137 |             "symbol2number",
- Found 328 diagnostics
+ Found 327 diagnostics
```
2025-03-10 13:24:13 +01:00
Dhruv Manilawala
f19cb86c5d Update migration guide with the new ruff.configuration (#16567)
## Summary

This PR updates the migration guide to use the new `ruff.configuration`
settings update to provide a better experience.

### Preview

<details><summary>Migration page screenshot</summary>
<p>

![Ruff Editors
Migration](https://github.com/user-attachments/assets/38062dbc-a4c5-44f1-8dba-53f7f5872d77)

</p>
</details>
2025-03-10 11:50:06 +00:00
David Peter
36d12cea47 [red-knot] Add 'mypy_primer' workflow (#16554)
## Summary

Run Red Knot on a small selection of ecosystem projects via a forked
version of `mypy_primer`.

Fork: https://github.com/astral-sh/mypy_primer
Branch: add-red-knot-support

## Test Plan

* Successful run:
https://github.com/astral-sh/ruff/actions/runs/13725319641/job/38390245552?pr=16554
* Intentially failed run where I commented out `unresolved-attribute`
diagnostics reporting:
https://github.com/astral-sh/ruff/actions/runs/13723777105/job/38385224144
2025-03-10 11:14:29 +01:00
renovate[bot]
a4396b3e6b Update Rust crate indoc to v2.0.6 (#16585)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [indoc](https://redirect.github.com/dtolnay/indoc) |
workspace.dependencies | patch | `2.0.5` -> `2.0.6` |

---

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

---

### Release Notes

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

###
[`v2.0.6`](https://redirect.github.com/dtolnay/indoc/releases/tag/2.0.6)

[Compare
Source](https://redirect.github.com/dtolnay/indoc/compare/2.0.5...2.0.6)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:26:02 +01:00
renovate[bot]
acfb920863 Update Rust crate syn to v2.0.100 (#16590)
This PR contains the following updates:

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

---

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

---

### Release Notes

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

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

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

- Add `Visit::visit_token_stream`, `VisitMut::visit_token_stream_mut`,
`Fold::fold_token_stream` for processing TokenStream during syntax tree
traversals
([#&#8203;1852](https://redirect.github.com/dtolnay/syn/issues/1852))

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

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

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:25:53 +01:00
renovate[bot]
08c48b15af Update Rust crate thiserror to v2.0.12 (#16591)
This PR contains the following updates:

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

---

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

---

### Release Notes

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

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

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

- Prevent elidable_lifetime_names pedantic clippy lint in generated impl
([#&#8203;413](https://redirect.github.com/dtolnay/thiserror/issues/413))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:25:40 +01:00
renovate[bot]
a7095c4196 Update Rust crate serde_json to v1.0.140 (#16589)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.139` -> `1.0.140` |

---

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

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.140`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.140)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.139...v1.0.140)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:25:30 +01:00
renovate[bot]
e79f9171cf Update Rust crate quote to v1.0.39 (#16587)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [quote](https://redirect.github.com/dtolnay/quote) |
workspace.dependencies | patch | `1.0.38` -> `1.0.39` |

---

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

---

### Release Notes

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

###
[`v1.0.39`](https://redirect.github.com/dtolnay/quote/releases/tag/1.0.39)

[Compare
Source](https://redirect.github.com/dtolnay/quote/compare/1.0.38...1.0.39)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:25:21 +01:00
renovate[bot]
e517b44a0a Update Rust crate serde to v1.0.219 (#16588)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.218` -> `1.0.219` |

---

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

---

### Release Notes

<details>
<summary>serde-rs/serde (serde)</summary>

###
[`v1.0.219`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.219)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.218...v1.0.219)

- Prevent `absolute_paths` Clippy restriction being triggered inside
macro-generated code
([#&#8203;2906](https://redirect.github.com/serde-rs/serde/issues/2906),
thanks [@&#8203;davidzeng0](https://redirect.github.com/davidzeng0))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:25:07 +01:00
renovate[bot]
1275665ecb Update Rust crate proc-macro2 to v1.0.94 (#16586)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [proc-macro2](https://redirect.github.com/dtolnay/proc-macro2) |
workspace.dependencies | patch | `1.0.93` -> `1.0.94` |

---

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

---

### Release Notes

<details>
<summary>dtolnay/proc-macro2 (proc-macro2)</summary>

###
[`v1.0.94`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.94)

[Compare
Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.93...1.0.94)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:24:43 +01:00
renovate[bot]
f2b41306d0 Update Rust crate anyhow to v1.0.97 (#16584)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.96` -> `1.0.97` |

---

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

---

### Release Notes

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

###
[`v1.0.97`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.97)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.96...1.0.97)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:24:34 +01:00
renovate[bot]
b02a42d99a Update dependency ruff to v0.9.10 (#16593)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [ruff](https://docs.astral.sh/ruff)
([source](https://redirect.github.com/astral-sh/ruff),
[changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md))
| `==0.9.9` -> `==0.9.10` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.9.10?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/ruff/0.9.10?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/ruff/0.9.9/0.9.10?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.9.9/0.9.10?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

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

---

### Release Notes

<details>
<summary>astral-sh/ruff (ruff)</summary>

###
[`v0.9.10`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0910)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.9.9...0.9.10)

##### Preview features

- \[`ruff`] Add new rule `RUF059`: Unused unpacked assignment
([#&#8203;16449](https://redirect.github.com/astral-sh/ruff/pull/16449))
- \[`syntax-errors`] Detect assignment expressions before Python 3.8
([#&#8203;16383](https://redirect.github.com/astral-sh/ruff/pull/16383))
- \[`syntax-errors`] Named expressions in decorators before Python 3.9
([#&#8203;16386](https://redirect.github.com/astral-sh/ruff/pull/16386))
- \[`syntax-errors`] Parenthesized keyword argument names after Python
3.8
([#&#8203;16482](https://redirect.github.com/astral-sh/ruff/pull/16482))
- \[`syntax-errors`] Positional-only parameters before Python 3.8
([#&#8203;16481](https://redirect.github.com/astral-sh/ruff/pull/16481))
- \[`syntax-errors`] Tuple unpacking in `return` and `yield` before
Python 3.8
([#&#8203;16485](https://redirect.github.com/astral-sh/ruff/pull/16485))
- \[`syntax-errors`] Type parameter defaults before Python 3.13
([#&#8203;16447](https://redirect.github.com/astral-sh/ruff/pull/16447))
- \[`syntax-errors`] Type parameter lists before Python 3.12
([#&#8203;16479](https://redirect.github.com/astral-sh/ruff/pull/16479))
- \[`syntax-errors`] `except*` before Python 3.11
([#&#8203;16446](https://redirect.github.com/astral-sh/ruff/pull/16446))
- \[`syntax-errors`] `type` statements before Python 3.12
([#&#8203;16478](https://redirect.github.com/astral-sh/ruff/pull/16478))

##### Bug fixes

- Escape template filenames in glob patterns in configuration
([#&#8203;16407](https://redirect.github.com/astral-sh/ruff/pull/16407))
- \[`flake8-simplify`] Exempt unittest context methods for `SIM115` rule
([#&#8203;16439](https://redirect.github.com/astral-sh/ruff/pull/16439))
- Formatter: Fix syntax error location in notebooks
([#&#8203;16499](https://redirect.github.com/astral-sh/ruff/pull/16499))
- \[`pyupgrade`] Do not offer fix when at least one target is
`global`/`nonlocal` (`UP028`)
([#&#8203;16451](https://redirect.github.com/astral-sh/ruff/pull/16451))
- \[`flake8-builtins`] Ignore variables matching module attribute names
(`A001`)
([#&#8203;16454](https://redirect.github.com/astral-sh/ruff/pull/16454))
- \[`pylint`] Convert `code` keyword argument to a positional argument
in fix for (`PLR1722`)
([#&#8203;16424](https://redirect.github.com/astral-sh/ruff/pull/16424))

##### CLI

- Move rule code from `description` to `check_name` in GitLab output
serializer
([#&#8203;16437](https://redirect.github.com/astral-sh/ruff/pull/16437))

##### Documentation

- \[`pydocstyle`] Clarify that `D417` only checks docstrings with an
arguments section
([#&#8203;16494](https://redirect.github.com/astral-sh/ruff/pull/16494))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:24:24 +01:00
renovate[bot]
79443d71eb Update Rust crate unicode-ident to v1.0.18 (#16592)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [unicode-ident](https://redirect.github.com/dtolnay/unicode-ident) |
workspace.dependencies | patch | `1.0.17` -> `1.0.18` |

---

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

---

### Release Notes

<details>
<summary>dtolnay/unicode-ident (unicode-ident)</summary>

###
[`v1.0.18`](https://redirect.github.com/dtolnay/unicode-ident/releases/tag/1.0.18)

[Compare
Source](https://redirect.github.com/dtolnay/unicode-ident/compare/1.0.17...1.0.18)

-   Documentation improvements

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:23:32 +01:00
David Peter
ca974706dd [red-knot] Do not ignore typeshed stubs for 'venv' module (#16596)
## Summary

We currently fail to add the stubs for the `venv` stdlib module because
there is a `venv/` ignore pattern in the top-level `.gitignore` file.

## Test Plan

Ran the typeshed sync workflow manually once to see if the `venv/`
folder is now correctly added.
2025-03-10 09:07:48 +01:00
Alex Waygood
b6c7ba4f8e [red-knot] Reduce Salsa lookups in Type::find_name_in_mro (#16582)
## Summary

Theoretically this should be slightly more performant, since the
`class.is_known()` calls each do a separate Salsa lookup, which we can
avoid if we do a single `match` on the value of `class.known()`. It also
ends up being two lines less code overall!

## Test Plan

`cargo test -p red_knot_python_semantic`
2025-03-10 07:55:22 +01:00
Alex Waygood
c970b794d0 Fix broken red-knot property tests (#16574)
## Summary

Fixes #16566, fixes #16575

The semantics of `Type::class_member` changed in
https://github.com/astral-sh/ruff/pull/16416, but the property-test
infrastructure was not updated. That means that the property tests were
panicking on the second `expect_type` call here:


0361021863/crates/red_knot_python_semantic/src/types/property_tests.rs (L151-L158)

With the somewhat unhelpful message:

```
Expected a (possibly unbound) type, not an unbound symbol
```

Applying this patch, and then running `QUICKCHECK_TESTS=1000000 cargo
test --release -p red_knot_python_semantic -- --ignored
types::property_tests::stable::equivalent_to_is_reflexive` showed
clearly that it was no longer able to find _any_ methods on _any_
classes due to the change in semantics of `Type::class_member`:

```diff
--- a/crates/red_knot_python_semantic/src/types/property_tests.rs
+++ b/crates/red_knot_python_semantic/src/types/property_tests.rs
@@ -27,7 +27,7 @@
 use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
 
 use crate::db::tests::{setup_db, TestDb};
-use crate::symbol::{builtins_symbol, known_module_symbol};
+use crate::symbol::{builtins_symbol, known_module_symbol, Symbol};
 use crate::types::{
     BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType,
     SubclassOfType, TupleType, Type, UnionType,
@@ -150,10 +150,11 @@ impl Ty {
             Ty::BuiltinsFunction(name) => builtins_symbol(db, name).symbol.expect_type(),
             Ty::BuiltinsBoundMethod { class, method } => {
                 let builtins_class = builtins_symbol(db, class).symbol.expect_type();
-                let function = builtins_class
-                    .class_member(db, method.into())
-                    .symbol
-                    .expect_type();
+                let Symbol::Type(function, ..) =
+                    builtins_class.class_member(db, method.into()).symbol
+                else {
+                    panic!("no method `{method}` on class `{class}`");
+                };
 
                 create_bound_method(db, function, builtins_class)
             }
```

This PR updates the property-test infrastructure to use `Type::member`
rather than `Type::class_member`.

## Test Plan

- Ran `QUICKCHECK_TESTS=1000000 cargo test --release -p
red_knot_python_semantic -- --ignored types::property_tests::stable`
successfully
- Checked that there were no remaining uses of `Type::class_member` in
`property_tests.rs`
2025-03-09 17:40:08 +00:00
Alex Waygood
335b264fe2 [red-knot] Consistent spelling of "metaclass" and "meta-type" (#16576)
## Summary

Fixes a small nit of mine -- we are currently inconsistent in our
spelling between "metaclass" and "meta class", and between "meta type"
and "meta-type". This PR means that we consistently use "metaclass" and
"meta-type".

## Test Plan

`uvx pre-commit run -a`
2025-03-09 12:30:32 +00:00
Dhruv Manilawala
0361021863 [red-knot] Understand typing.Callable (#16493)
## Summary

Part of https://github.com/astral-sh/ruff/issues/15382

This PR implements a general callable type that wraps around a
`Signature` and it uses that new type to represent `typing.Callable`.

It also implements `Display` support for `Callable`. The format is as:
```
([<arg name>][: <arg type>][ = <default type>], ...) -> <return type>
```

The `/` and `*` separators are added at the correct boundary for
positional-only and keyword-only parameters. Now, as `typing.Callable`
only has positional-only parameters, the rendered signature would be:

```py
Callable[[int, str], None]
# (int, str, /) -> None
```

The `/` separator represents that all the arguments are positional-only.

The relationship methods that check assignability, subtype relationship,
etc. are not yet implemented and will be done so as a follow-up.

## Test Plan

Add test cases for display support for `Signature` and various mdtest
for `typing.Callable`.
2025-03-08 03:58:52 +00:00
Eric Mark Martin
24c8b1242e [red-knot] Support unpacking with target (#16469)
## Summary

Resolves #16365

Add support for unpacking `with` statement targets.

## Test Plan

Added some test cases, alike the ones added by #15058.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-03-08 02:36:35 +00:00
David Peter
820a31af5d [red-knot] Attribute access and the descriptor protocol (#16416)
## Summary

* Attributes/method are now properly looked up on metaclasses, when
called on class objects
* We properly distinguish between data descriptors and non-data
descriptors (but we do not yet support them in store-context, i.e.
`obj.data_descr = …`)
* The descriptor protocol is now implemented in a single unified place
for instances, classes and dunder-calls. Unions and possibly-unbound
symbols are supported in all possible stages of the process by creating
union types as results.
* In general, the handling of "possibly-unbound" symbols has been
improved in a lot of places: meta-class attributes, attributes,
descriptors with possibly-unbound `__get__` methods, instance
attributes, …
* We keep track of type qualifiers in a lot more places. I anticipate
that this will be useful if we import e.g. `Final` symbols from other
modules (see relevant change to typing spec:
https://github.com/python/typing/pull/1937).
* Detection and special-casing of the `typing.Protocol` special form in
order to avoid lots of changes in the test suite due to new `@Todo`
types when looking up attributes on builtin types which have `Protocol`
in their MRO. We previously
looked up attributes in a wrong way, which is why this didn't come up
before.

closes #16367
closes #15966

## Context

The way attribute lookup in `Type::member` worked before was simply
wrong (mostly my own fault). The whole instance-attribute lookup should
probably never have been integrated into `Type::member`. And the
`Type::static_member` function that I introduced in my last descriptor
PR was the wrong abstraction. It's kind of fascinating how far this
approach took us, but I am pretty confident that the new approach
proposed here is what we need to model this correctly.

There are three key pieces that are required to implement attribute
lookups:

- **`Type::class_member`**/**`Type::find_in_mro`**: The
`Type::find_in_mro` method that can look up attributes on class bodies
(and corresponding bases). This is a partial function on types, as it
can not be called on instance types like`Type::Instance(…)` or
`Type::IntLiteral(…)`. For this reason, we usually call it through
`Type::class_member`, which is essentially just
`type.to_meta_type().find_in_mro(…)` plus union/intersection handling.
- **`Type::instance_member`**: This new function is basically the
type-level equivalent to `obj.__dict__[name]` when called on
`Type::Instance(…)`. We use this to discover instance attributes such as
those that we see as declarations on class bodies or as (annotated)
assignments to `self.attr` in methods of a class.
- The implementation of the descriptor protocol. It works slightly
different for instances and for class objects, but it can be described
by the general framework:
- Call `type.class_member("attribute")` to look up "attribute" in the
MRO of the meta type of `type`. Call the resulting `Symbol` `meta_attr`
(even if it's unbound).
- Use `meta_attr.class_member("__get__")` to look up `__get__` on the
*meta type* of `meta_attr`. Call it with `__get__(meta_attr, self,
self.to_meta_type())`. If this fails (either the lookup or the call),
just proceed with `meta_attr`. Otherwise, replace `meta_attr` in the
following with the return type of `__get__`. In this step, we also probe
if a `__set__` or `__delete__` method exists and store it in
`meta_attr_kind` (can be either "data descriptor" or "normal attribute
or non-data descriptor").
  - Compute a `fallback` type.
    - For instances, we use `self.instance_member("attribute")`
- For class objects, we use `class_attr =
self.find_in_mro("attribute")`, and then try to invoke the descriptor
protocol on `class_attr`, i.e. we look up `__get__` on the meta type of
`class_attr` and call it with `__get__(class_attr, None, self)`. This
additional invocation of the descriptor protocol on the fallback type is
one major asymmetry in the otherwise universal descriptor protocol
implementation.
- Finally, we look at `meta_attr`, `meta_attr_kind` and `fallback`, and
handle various cases of (possible) unboundness of these symbols.
- If `meta_attr` is bound and a data descriptor, just return `meta_attr`
- If `meta_attr` is not a data descriptor, and `fallback` is bound, just
return `fallback`
- If `meta_attr` is not a data descriptor, and `fallback` is unbound,
return `meta_attr`
- Return unions of these three possibilities for partially-bound
symbols.

This allows us to handle class objects and instances within the same
framework. There is a minor additional detail where for instances, we do
not allow the fallback type (the instance attribute) to completely
shadow the non-data descriptor. We do this because we (currently) don't
want to pretend that we can statically infer that an instance attribute
is always set.

Dunder method calls can also be embedded into this framework. The only
thing that changes is that *there is no fallback type*. If a dunder
method is called on an instance, we do not fall back to instance
variables. If a dunder method is called on a class object, we only look
it up on the meta class, never on the class itself.

## Test Plan

New Markdown tests.
2025-03-07 22:03:28 +01:00
InSync
a18d8bfa7d [pep8-naming] Add links to ignore-names options in various rules' documentation (#16557)
## Summary

Resolves #16551.

All rules using
[`lint.pep8-naming.ignore-names`](https://docs.astral.sh/ruff/settings/#lint_pep8-naming_ignore-names)
and
[`lint.pep8-naming.extend-ignore-names`](https://docs.astral.sh/ruff/settings/#lint_pep8-naming_extend-ignore-names)
now have their documentation linked to these two options.

## Test Plan

None.
2025-03-07 14:49:08 -05:00
Shunsuke Shibayama
348c196cb3 [red-knot] avoid inferring types if unpacking fails (#16530)
## Summary

This PR closes #15199.

The change I just made is to set all variables to type `Unknown` if
unpacking fails, but in some cases this may be excessive.
For example:

```py
a, b, c = "ab"
reveal_type(a)  # Unknown, but it would be reasonable to think of it as LiteralString
reveal_type(c)  # Unknown
```

```py
# Failed to unpack before the starred expression
(a, b, *c, d, e) = (1,)
reveal_type(a)  # Unknown
reveal_type(b)  # Unknown
...
# Failed to unpack after the starred expression
(a, b, *c, d, e) = (1, 2, 3)
reveal_type(a)  # Unknown, but should it be Literal[1]?
reveal_type(b)  # Unknown, but should it be Literal[2]?
reveal_type(c)  # Todo
reveal_type(d)  # Unknown
reveal_type(e)  # Unknown
```

I will modify it if you think it would be better to make it a different
type than just `Unknown`.

## Test Plan

I have made appropriate modifications to the test cases affected by this
change, and also added some more test cases.
2025-03-07 11:04:44 -08:00
Vasco Schiavo
6d6e524b90 [flake8-bandit] Fix mixed-case hash algorithm names (S324) (#16552)
The PR solves issue #16525
2025-03-07 15:21:07 +00:00
69 changed files with 4120 additions and 1031 deletions

93
.github/workflows/mypy_primer.yaml vendored Normal file
View File

@@ -0,0 +1,93 @@
name: Run mypy_primer
permissions: {}
on:
pull_request:
paths:
- "crates/red_knot*/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:
mypy_primer:
name: Run mypy_primer
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
- uses: Swatinem/rust-cache@v2
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
- name: Run mypy_primer
shell: bash
run: |
cd ruff
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b base_commit "$MERGE_BASE"
echo "base commit"
git rev-list --format=%s --max-count=1 base_commit
cd ..
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx mypy_primer \
--repo ruff \
--type-checker knot \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow)$' \
--output concise \
--debug > mypy_primer.diff || [ $? -eq 1 ]
# Output diff with ANSI color codes
cat mypy_primer.diff
# Remove ANSI color codes before uploading
sed -ie 's/\x1b\[[0-9;]*m//g' mypy_primer.diff
echo ${{ github.event.number }} > pr-number
- name: Upload diff
uses: actions/upload-artifact@v4
with:
name: mypy_primer_diff
path: mypy_primer.diff
- name: Upload pr-number
uses: actions/upload-artifact@v4
with:
name: pr-number
path: pr-number

View File

@@ -0,0 +1,97 @@
name: PR comment (mypy_primer)
on: # zizmor: ignore[dangerous-triggers]
workflow_run:
workflows: [Run mypy_primer]
types: [completed]
workflow_dispatch:
inputs:
workflow_run_id:
description: The mypy_primer workflow that triggers the workflow run
required: true
jobs:
comment:
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@v8
name: Download PR number
with:
name: pr-number
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true
- name: Parse pull request number
id: pr-number
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@v8
name: "Download mypy_primer results"
id: download-mypy_primer_diff
if: steps.pr-number.outputs.pr-number
with:
name: mypy_primer_diff
workflow: mypy_primer.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/mypy_primer_diff
workflow_conclusion: completed
if_no_artifact_found: ignore
allow_forks: true
- name: Generate comment content
id: generate-comment
if: steps.download-mypy_primer_diff.outputs.found_artifact == 'true'
run: |
# Guard against malicious mypy_primer results that symlink to a secret
# file on this runner
if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]]
then
echo "Error: mypy_primer.diff cannot be a symlink"
exit 1
fi
# Note this identifier is used to find the comment to update on
# subsequent runs
echo '<!-- generated-comment mypy_primer -->' >> comment.txt
echo '## `mypy_primer` results' >> comment.txt
if [ -s "pr/mypy_primer_diff/mypy_primer.diff" ]; then
echo '<details>' >> comment.txt
echo '<summary>Changes were detected when running on open source projects</summary>' >> comment.txt
echo '' >> comment.txt
echo '```diff' >> comment.txt
cat pr/mypy_primer_diff/mypy_primer.diff >> comment.txt
echo '```' >> comment.txt
echo '</details>' >> comment.txt
else
echo 'No ecosystem changes detected ✅' >> comment.txt
fi
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@v3
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
issue-number: ${{ steps.pr-number.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: "<!-- generated-comment mypy_primer -->"
- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}
body-path: comment.txt
edit-mode: replace

140
Cargo.lock generated
View File

@@ -124,9 +124,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.96"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "argfile"
@@ -399,7 +399,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -416,7 +416,7 @@ checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
dependencies = [
"nix",
"terminfo",
"thiserror 2.0.11",
"thiserror 2.0.12",
"which",
"windows-sys 0.59.0",
]
@@ -690,7 +690,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -701,7 +701,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -771,7 +771,7 @@ dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -803,7 +803,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -1295,7 +1295,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -1387,9 +1387,9 @@ dependencies = [
[[package]]
name = "indoc"
version = "2.0.5"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "inotify"
@@ -1460,7 +1460,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -1602,7 +1602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf66548c351bcaed792ef3e2b430cc840fbde504e09da6b29ed114ca60dcd4b"
dependencies = [
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -2081,7 +2081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror 2.0.11",
"thiserror 2.0.12",
"ucd-trie",
]
@@ -2105,7 +2105,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -2174,7 +2174,7 @@ checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -2243,9 +2243,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.93"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
@@ -2275,7 +2275,7 @@ dependencies = [
"newtype-uuid",
"quick-xml",
"strip-ansi-escapes",
"thiserror 2.0.11",
"thiserror 2.0.12",
"uuid",
]
@@ -2310,9 +2310,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.38"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
dependencies = [
"proc-macro2",
]
@@ -2453,7 +2453,7 @@ dependencies = [
"salsa",
"schemars",
"serde",
"thiserror 2.0.11",
"thiserror 2.0.12",
"toml",
"tracing",
]
@@ -2499,7 +2499,7 @@ dependencies = [
"strum_macros",
"tempfile",
"test-case",
"thiserror 2.0.11",
"thiserror 2.0.12",
"tracing",
]
@@ -2551,7 +2551,7 @@ dependencies = [
"serde",
"smallvec",
"tempfile",
"thiserror 2.0.11",
"thiserror 2.0.12",
"toml",
]
@@ -2707,7 +2707,7 @@ dependencies = [
"strum",
"tempfile",
"test-case",
"thiserror 2.0.11",
"thiserror 2.0.12",
"tikv-jemallocator",
"toml",
"tracing",
@@ -2790,7 +2790,7 @@ dependencies = [
"schemars",
"serde",
"tempfile",
"thiserror 2.0.11",
"thiserror 2.0.12",
"tracing",
"tracing-subscriber",
"tracing-tree",
@@ -2945,7 +2945,7 @@ dependencies = [
"strum",
"strum_macros",
"test-case",
"thiserror 2.0.11",
"thiserror 2.0.12",
"toml",
"typed-arena",
"unicode-normalization",
@@ -2962,7 +2962,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -2979,7 +2979,7 @@ dependencies = [
"serde_json",
"serde_with",
"test-case",
"thiserror 2.0.11",
"thiserror 2.0.12",
"uuid",
]
@@ -3053,7 +3053,7 @@ dependencies = [
"similar",
"smallvec",
"static_assertions",
"thiserror 2.0.11",
"thiserror 2.0.12",
"tracing",
]
@@ -3189,7 +3189,7 @@ dependencies = [
"serde",
"serde_json",
"shellexpand",
"thiserror 2.0.11",
"thiserror 2.0.12",
"toml",
"tracing",
"tracing-subscriber",
@@ -3359,7 +3359,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
"synstructure",
]
@@ -3393,7 +3393,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -3416,9 +3416,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.218"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
@@ -3436,13 +3436,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.218"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -3453,14 +3453,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
name = "serde_json"
version = "1.0.139"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
@@ -3476,7 +3476,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -3517,7 +3517,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -3648,7 +3648,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -3664,9 +3664,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.98"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
@@ -3681,7 +3681,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -3753,7 +3753,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -3764,7 +3764,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
"test-case-core",
]
@@ -3779,11 +3779,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.11"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.11",
"thiserror-impl 2.0.12",
]
[[package]]
@@ -3794,18 +3794,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
name = "thiserror-impl"
version = "2.0.11"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -3936,7 +3936,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -4087,9 +4087,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-normalization"
@@ -4203,7 +4203,7 @@ checksum = "d28dd23acb5f2fa7bd2155ab70b960e770596b3bb6395119b40476c3655dfba4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -4325,7 +4325,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
"wasm-bindgen-shared",
]
@@ -4360,7 +4360,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4395,7 +4395,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -4510,7 +4510,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -4521,7 +4521,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -4759,7 +4759,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
"synstructure",
]
@@ -4790,7 +4790,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -4801,7 +4801,7 @@ checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]
@@ -4821,7 +4821,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
"synstructure",
]
@@ -4844,7 +4844,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"syn 2.0.100",
]
[[package]]

View File

@@ -216,6 +216,17 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
self.visit_body(&for_stmt.orelse);
return;
}
Stmt::With(with_stmt) => {
for item in &with_stmt.items {
if let Some(target) = &item.optional_vars {
self.visit_target(target);
}
self.visit_expr(&item.context_expr);
}
self.visit_body(&with_stmt.body);
return;
}
Stmt::AnnAssign(_)
| Stmt::Return(_)
| Stmt::Delete(_)
@@ -223,7 +234,6 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
| Stmt::TypeAlias(_)
| Stmt::While(_)
| Stmt::If(_)
| Stmt::With(_)
| Stmt::Match(_)
| Stmt::Raise(_)
| Stmt::Try(_)

View File

@@ -0,0 +1,195 @@
# Callable
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>.
## Invalid forms
The `Callable` special form requires _exactly_ two arguments where the first argument is either a
parameter type list, parameter specification, `typing.Concatenate`, or `...` and the second argument
is the return type. Here, we explore various invalid forms.
### Empty
A bare `Callable` without any type arguments:
```py
from typing import Callable
def _(c: Callable):
reveal_type(c) # revealed: (...) -> Unknown
```
### Invalid parameter type argument
When it's not a list:
```py
from typing import Callable
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
def _(c: Callable[int, str]):
reveal_type(c) # revealed: (...) -> Unknown
```
Or, when it's a literal type:
```py
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
def _(c: Callable[42, str]):
reveal_type(c) # revealed: (...) -> Unknown
```
Or, when one of the parameter type is invalid in the list:
```py
def _(c: Callable[[int, 42, str, False], None]):
# revealed: (int, @Todo(number literal in type expression), str, @Todo(boolean literal in type expression), /) -> None
reveal_type(c)
```
### Missing return type
Using a parameter list:
```py
from typing import Callable
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[[int, str]]):
reveal_type(c) # revealed: (int, str, /) -> Unknown
```
Or, an ellipsis:
```py
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[...]):
reveal_type(c) # revealed: (...) -> Unknown
```
### More than two arguments
We can't reliably infer the callable type if there are more then 2 arguments because we don't know
which argument corresponds to either the parameters or the return type.
```py
from typing import Callable
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[[int], str, str]):
reveal_type(c) # revealed: (...) -> Unknown
```
## Simple
A simple `Callable` with multiple parameters and a return type:
```py
from typing import Callable
def _(c: Callable[[int, str], int]):
reveal_type(c) # revealed: (int, str, /) -> int
```
## Nested
A nested `Callable` as one of the parameter types:
```py
from typing import Callable
def _(c: Callable[[Callable[[int], str]], int]):
reveal_type(c) # revealed: ((int, /) -> str, /) -> int
```
And, as the return type:
```py
def _(c: Callable[[int, str], Callable[[int], int]]):
reveal_type(c) # revealed: (int, str, /) -> (int, /) -> int
```
## Gradual form
The `Callable` special form supports the use of `...` in place of the list of parameter types. This
is a [gradual form] indicating that the type is consistent with any input signature:
```py
from typing import Callable
def gradual_form(c: Callable[..., str]):
reveal_type(c) # revealed: (...) -> str
```
## Using `typing.Concatenate`
Using `Concatenate` as the first argument to `Callable`:
```py
from typing_extensions import Callable, Concatenate
def _(c: Callable[Concatenate[int, str, ...], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
And, as one of the parameter types:
```py
def _(c: Callable[[Concatenate[int, str, ...], int], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
## Using `typing.ParamSpec`
Using a `ParamSpec` in a `Callable` annotation:
```py
from typing_extensions import Callable
# TODO: Not an error; remove once `ParamSpec` is supported
# error: [invalid-type-form]
def _[**P1](c: Callable[P1, int]):
reveal_type(c) # revealed: (...) -> Unknown
```
And, using the legacy syntax:
```py
from typing_extensions import ParamSpec
P2 = ParamSpec("P2")
# TODO: Not an error; remove once `ParamSpec` is supported
# error: [invalid-type-form]
def _(c: Callable[P2, int]):
reveal_type(c) # revealed: (...) -> Unknown
```
## Using `typing.Unpack`
Using the unpack operator (`*`):
```py
from typing_extensions import Callable, TypeVarTuple
Ts = TypeVarTuple("Ts")
def _(c: Callable[[int, *Ts], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
And, using the legacy syntax using `Unpack`:
```py
from typing_extensions import Unpack
def _(c: Callable[[int, Unpack[Ts]], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
[gradual form]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-gradual-form

View File

@@ -73,12 +73,12 @@ qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(overloaded method)
reveal_type(foo.join(qux)) # revealed: @Todo(return type of decorated function)
template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
# TODO: Infer `LiteralString`
reveal_type(template.format(foo, bar)) # revealed: @Todo(overloaded method)
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of decorated function)
```
### Assignability

View File

@@ -70,8 +70,7 @@ import typing
class ListSubclass(typing.List): ...
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[ListSubclass], Literal[list], Unknown, Literal[object]]
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...

View File

@@ -29,6 +29,8 @@ 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

@@ -75,8 +75,7 @@ def _(flag: bool):
f = Foo()
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
# that `Foo.__iadd__` may be unbound as additional context.
# error: [unsupported-operator] "Operator `+=` is unsupported between objects of type `Foo` and `Literal["Hello, world!"]`"
f += "Hello, world!"
reveal_type(f) # revealed: int | Unknown

View File

@@ -155,7 +155,9 @@ reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: str | None
# TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API,
# which is planned in https://github.com/astral-sh/ruff/issues/14297
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
```
@@ -356,9 +358,25 @@ class C:
c_instance = C()
# TODO: Should be `Unknown | int | None`
# error: [unresolved-attribute]
reveal_type(c_instance.x) # revealed: Unknown
reveal_type(c_instance.x) # revealed: Unknown | int | None
```
#### Attributes defined in `with` statements, but with unpacking
```py
class ContextManager:
def __enter__(self) -> tuple[int | None, int]: ...
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
class C:
def __init__(self) -> None:
with ContextManager() as (self.x, self.y):
pass
c_instance = C()
reveal_type(c_instance.x) # revealed: Unknown | int | None
reveal_type(c_instance.y) # revealed: Unknown | int
```
#### Attributes defined in comprehensions
@@ -704,8 +722,91 @@ reveal_type(Derived().declared_in_body) # revealed: int | None
reveal_type(Derived().defined_in_init) # revealed: str | None
```
## Accessing attributes on class objects
When accessing attributes on class objects, they are always looked up on the type of the class
object first, i.e. on the metaclass:
```py
from typing import Literal
class Meta1:
attr: Literal["metaclass value"] = "metaclass value"
class C1(metaclass=Meta1): ...
reveal_type(C1.attr) # revealed: Literal["metaclass value"]
```
However, the metaclass attribute only takes precedence over a class-level attribute if it is a data
descriptor. If it is a non-data descriptor or a normal attribute, the class-level attribute is used
instead (see the [descriptor protocol tests] for data/non-data descriptor attributes):
```py
class Meta2:
attr: str = "metaclass value"
class C2(metaclass=Meta2):
attr: Literal["class value"] = "class value"
reveal_type(C2.attr) # revealed: Literal["class value"]
```
If the class-level attribute is only partially defined, we union the metaclass attribute with the
class-level attribute:
```py
def _(flag: bool):
class Meta3:
attr1 = "metaclass value"
attr2: Literal["metaclass value"] = "metaclass value"
class C3(metaclass=Meta3):
if flag:
attr1 = "class value"
# TODO: Neither mypy nor pyright show an error here, but we could consider emitting a conflicting-declaration diagnostic here.
attr2: Literal["class value"] = "class value"
reveal_type(C3.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
reveal_type(C3.attr2) # revealed: Literal["metaclass value", "class value"]
```
If the *metaclass* attribute is only partially defined, we emit a `possibly-unbound-attribute`
diagnostic:
```py
def _(flag: bool):
class Meta4:
if flag:
attr1: str = "metaclass value"
class C4(metaclass=Meta4): ...
# error: [possibly-unbound-attribute]
reveal_type(C4.attr1) # revealed: str
```
Finally, if both the metaclass attribute and the class-level attribute are only partially defined,
we union them and emit a `possibly-unbound-attribute` diagnostic:
```py
def _(flag1: bool, flag2: bool):
class Meta5:
if flag1:
attr1 = "metaclass value"
class C5(metaclass=Meta5):
if flag2:
attr1 = "class value"
# error: [possibly-unbound-attribute]
reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
```
## Union of attributes
If the (meta)class is a union type or if the attribute on the (meta) class has a union type, we
infer those union types accordingly:
```py
def _(flag: bool):
if flag:
@@ -716,14 +817,35 @@ def _(flag: bool):
class C1:
x = 2
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
class C2:
if flag:
x = 3
else:
x = 4
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
if flag:
class Meta3(type):
x = 5
else:
class Meta3(type):
x = 6
class C3(metaclass=Meta3): ...
reveal_type(C3.x) # revealed: Unknown | Literal[5, 6]
class Meta4(type):
if flag:
x = 7
else:
x = 8
class C4(metaclass=Meta4): ...
reveal_type(C4.x) # revealed: Unknown | Literal[7, 8]
```
## Inherited class attributes
@@ -883,7 +1005,7 @@ def _(flag: bool):
self.x = 1
# error: [possibly-unbound-attribute]
reveal_type(Foo().x) # revealed: int
reveal_type(Foo().x) # revealed: int | Unknown
```
#### Possibly unbound
@@ -1105,8 +1227,8 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
bools are instances of that class:
```py
reveal_type(True.__and__) # revealed: @Todo(overloaded method)
reveal_type(False.__or__) # revealed: @Todo(overloaded method)
reveal_type(True.__and__) # revealed: <bound method `__and__` of `Literal[True]`>
reveal_type(False.__or__) # revealed: <bound method `__or__` of `Literal[False]`>
```
Some attributes are special-cased, however:
@@ -1262,6 +1384,7 @@ reveal_type(C.a_none) # revealed: None
Some of the tests in the *Class and instance variables* section draw inspiration from
[pyright's documentation] on this topic.
[descriptor protocol tests]: descriptor_protocol.md
[pyright's documentation]: https://microsoft.github.io/pyright/#/type-concepts-advanced?id=class-and-instance-variables
[typing spec on `classvar`]: https://typing.readthedocs.io/en/latest/spec/class-compat.html#classvar
[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar

View File

@@ -10,8 +10,7 @@ reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
# TODO: Should emit `unsupported-operator` but we don't understand the bases of `str`, so we think
# it inherits `Unknown`, so we think `str.__radd__` is `Unknown` instead of nonexistent.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[2]` and `Literal["f"]`"
reveal_type(2 + "f") # revealed: Unknown
def lhs(x: int):

View File

@@ -40,10 +40,21 @@ class Meta(type):
def __getitem__(cls, key: int) -> str:
return str(key)
class DunderOnMetaClass(metaclass=Meta):
class DunderOnMetaclass(metaclass=Meta):
pass
reveal_type(DunderOnMetaClass[0]) # revealed: str
reveal_type(DunderOnMetaclass[0]) # revealed: str
```
If the dunder method is only present on the class itself, it will not be called:
```py
class ClassWithNormalDunder:
def __getitem__(self, key: int) -> str:
return str(key)
# error: [non-subscriptable]
ClassWithNormalDunder[0]
```
## Operating on instances
@@ -79,13 +90,32 @@ reveal_type(this_fails[0]) # revealed: Unknown
However, the attached dunder method *can* be called if accessed directly:
```py
# TODO: `this_fails.__getitem__` is incorrectly treated as a bound method. This
# should be fixed with https://github.com/astral-sh/ruff/issues/16367
# error: [too-many-positional-arguments]
# error: [invalid-argument-type]
reveal_type(this_fails.__getitem__(this_fails, 0)) # revealed: Unknown | str
```
The instance-level method is also not called when the class-level method is present:
```py
def external_getitem1(instance, key) -> str:
return "a"
def external_getitem2(key) -> int:
return 1
def _(flag: bool):
class ThisFails:
if flag:
__getitem__ = external_getitem1
def __init__(self):
self.__getitem__ = external_getitem2
this_fails = ThisFails()
# error: [call-possibly-unbound-method]
reveal_type(this_fails[0]) # revealed: Unknown | str
```
## When the dunder is not a method
A dunder can also be a non-method callable:
@@ -126,3 +156,64 @@ class_with_descriptor_dunder = ClassWithDescriptorDunder()
reveal_type(class_with_descriptor_dunder[0]) # revealed: str
```
## Dunders can not be overwritten on instances
If we attempt to overwrite a dunder method on an instance, it does not affect the behavior of
implicit dunder calls:
```py
class C:
def __getitem__(self, key: int) -> str:
return str(key)
def f(self):
# TODO: This should emit an `invalid-assignment` diagnostic once we understand the type of `self`
self.__getitem__ = None
# This is still fine, and simply calls the `__getitem__` method on the class
reveal_type(C()[0]) # revealed: str
```
## Calling a union of dunder methods
```py
def _(flag: bool):
class C:
if flag:
def __getitem__(self, key: int) -> str:
return str(key)
else:
def __getitem__(self, key: int) -> bytes:
return key
c = C()
reveal_type(c[0]) # revealed: str | bytes
if flag:
class D:
def __getitem__(self, key: int) -> str:
return str(key)
else:
class D:
def __getitem__(self, key: int) -> bytes:
return key
d = D()
reveal_type(d[0]) # revealed: str | bytes
```
## Calling a possibly-unbound dunder method
```py
def _(flag: bool):
class C:
if flag:
def __getitem__(self, key: int) -> str:
return str(key)
c = C()
# error: [call-possibly-unbound-method]
reveal_type(c[0]) # revealed: str
```

View File

@@ -12,7 +12,7 @@ import inspect
class Descriptor:
def __get__(self, instance, owner) -> str:
return 1
return "a"
class C:
normal: int = 1
@@ -59,7 +59,7 @@ import sys
reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[1]
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[real]
```
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
@@ -72,6 +72,23 @@ class D:
reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int
```
And attributes on metaclasses can be accessed when probing the class:
```py
class Meta(type):
attr: int = 1
class E(metaclass=Meta): ...
reveal_type(inspect.getattr_static(E, "attr")) # revealed: int
```
Metaclass attributes can not be added when probing an instance of the class:
```py
reveal_type(inspect.getattr_static(E(), "attr", "non_existent")) # revealed: Literal["non_existent"]
```
## Error cases
We can only infer precise types if the attribute is a literal string. In all other cases, we fall

View File

@@ -255,6 +255,58 @@ method_wrapper()
method_wrapper(C(), C, "one too many")
```
## Fallback to metaclass
When a method is accessed on a class object, it is looked up on the metaclass if it is not found on
the class itself. This also creates a bound method that is bound to the class object itself:
```py
from __future__ import annotations
class Meta(type):
def f(cls, arg: int) -> str:
return "a"
class C(metaclass=Meta):
pass
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(C.f(1)) # revealed: str
```
The method `f` can not be accessed from an instance of the class:
```py
# error: [unresolved-attribute] "Type `C` has no attribute `f`"
C().f
```
A metaclass function can be shadowed by a method on the class:
```py
from typing import Any, Literal
class D(metaclass=Meta):
def f(arg: int) -> Literal["a"]:
return "a"
reveal_type(D.f(1)) # revealed: Literal["a"]
```
If the class method is possibly unbound, we union the return types:
```py
def flag() -> bool:
return True
class E(metaclass=Meta):
if flag():
def f(arg: int) -> Any:
return "a"
reveal_type(E.f(1)) # revealed: str | Any
```
## `@classmethod`
### Basic
@@ -371,10 +423,10 @@ class C:
# these should all return `str`:
reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function)
reveal_type(C().f1(1)) # revealed: @Todo(decorated method)
reveal_type(C().f1(1)) # revealed: @Todo(return type of decorated function)
reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function)
reveal_type(C().f2(1)) # revealed: @Todo(decorated method)
reveal_type(C().f2(1)) # revealed: @Todo(return type of decorated function)
```
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods

View File

@@ -0,0 +1,50 @@
# Call `type[...]`
## Single class
### Trivial constructor
```py
class C: ...
def _(subclass_of_c: type[C]):
reveal_type(subclass_of_c()) # revealed: C
```
### Non-trivial constructor
```py
class C:
def __init__(self, x: int): ...
def _(subclass_of_c: type[C]):
reveal_type(subclass_of_c(1)) # revealed: C
# TODO: Those should all be errors
reveal_type(subclass_of_c("a")) # revealed: C
reveal_type(subclass_of_c()) # revealed: C
reveal_type(subclass_of_c(1, 2)) # revealed: C
```
## Dynamic base
```py
from typing import Any
from knot_extensions import Unknown
def _(subclass_of_any: type[Any], subclass_of_unknown: type[Unknown]):
reveal_type(subclass_of_any()) # revealed: Any
reveal_type(subclass_of_any("any", "args", 1, 2)) # revealed: Any
reveal_type(subclass_of_unknown()) # revealed: Unknown
reveal_type(subclass_of_unknown("any", "args", 1, 2)) # revealed: Unknown
```
## Unions of classes
```py
class A: ...
class B: ...
def _(subclass_of_ab: type[A | B]):
reveal_type(subclass_of_ab()) # revealed: A | B
```

View File

@@ -31,16 +31,12 @@ reveal_type(c.ten) # revealed: Literal[10]
reveal_type(C.ten) # revealed: Literal[10]
# These are fine:
# TODO: This should not be an error
c.ten = 10 # error: [invalid-assignment]
c.ten = 10
C.ten = 10
# TODO: This should be an error (as the wrong type is being implicitly passed to `Ten.__set__`),
# but the error message is misleading.
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`"
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
c.ten = 11
# TODO: same as above
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
C.ten = 11
```
@@ -67,16 +63,14 @@ c = C()
reveal_type(c.flexible_int) # revealed: int | None
# TODO: These should not be errors
# error: [invalid-assignment]
c.flexible_int = 42 # okay
# TODO: This should not be an error
# error: [invalid-assignment]
c.flexible_int = "42" # also okay!
reveal_type(c.flexible_int) # revealed: int | None
# TODO: This should be an error, but the message needs to be improved.
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `flexible_int` of type `FlexibleInt`"
# TODO: This should be an error
c.flexible_int = None # not okay
reveal_type(c.flexible_int) # revealed: int | None
@@ -84,11 +78,10 @@ reveal_type(c.flexible_int) # revealed: int | None
## Data and non-data descriptors
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example\
of a data descriptor is a `property` with a setter and/or a deleter.\
Descriptors that only define `__get__`, meanwhile, are called *non-data descriptors*. Examples
include\
functions, `classmethod` or `staticmethod`).
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example of a
data descriptor is a `property` with a setter and/or a deleter. Descriptors that only define
`__get__`, meanwhile, are called *non-data descriptors*. Examples include functions, `classmethod`
or `staticmethod`.
The precedence chain for attribute access is (1) data descriptors, (2) instance attributes, and (3)
non-data descriptors.
@@ -100,7 +93,7 @@ class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
return "data"
def __set__(self, instance: int, value) -> None:
def __set__(self, instance: object, value: int) -> None:
pass
class NonDataDescriptor:
@@ -124,12 +117,7 @@ class C:
c = C()
# TODO: This should ideally be `Unknown | Literal["data"]`.
#
# - Pyright also wrongly shows `int | Literal['data']` here
# - Mypy shows Literal["data"] here, but also shows Literal["non-data"] below.
#
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data", 1]
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data"]
reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1]
@@ -143,6 +131,230 @@ reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
C.data_descriptor = "something else" # This is okay
```
## Descriptor protocol for class objects
When attributes are accessed on a class object, the following [precedence chain] is used:
- Data descriptor on the metaclass
- Data or non-data descriptor on the class
- Class attribute
- Non-data descriptor on the metaclass
- Metaclass attribute
To verify this, we define a data and a non-data descriptor:
```py
from typing import Literal, Any
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
return "data"
def __set__(self, instance: object, value: str) -> None:
pass
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
return "non-data"
```
First, we make sure that the descriptors are correctly accessed when defined on the metaclass or the
class:
```py
class Meta1(type):
meta_data_descriptor: DataDescriptor = DataDescriptor()
meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
class C1(metaclass=Meta1):
class_data_descriptor: DataDescriptor = DataDescriptor()
class_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
reveal_type(C1.meta_data_descriptor) # revealed: Literal["data"]
reveal_type(C1.meta_non_data_descriptor) # revealed: Literal["non-data"]
reveal_type(C1.class_data_descriptor) # revealed: Literal["data"]
reveal_type(C1.class_non_data_descriptor) # revealed: Literal["non-data"]
```
Next, we demonstrate that a *metaclass data descriptor* takes precedence over all class-level
attributes:
```py
class Meta2(type):
meta_data_descriptor1: DataDescriptor = DataDescriptor()
meta_data_descriptor2: DataDescriptor = DataDescriptor()
class ClassLevelDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["class level data descriptor"]:
return "class level data descriptor"
def __set__(self, instance: object, value: str) -> None:
pass
class C2(metaclass=Meta2):
meta_data_descriptor1: Literal["value on class"] = "value on class"
meta_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
reveal_type(C2.meta_data_descriptor1) # revealed: Literal["data"]
reveal_type(C2.meta_data_descriptor2) # revealed: Literal["data"]
```
On the other hand, normal metaclass attributes and metaclass non-data descriptors are shadowed by
class-level attributes (descriptor or not):
```py
class Meta3(type):
meta_attribute1: Literal["value on metaclass"] = "value on metaclass"
meta_attribute2: Literal["value on metaclass"] = "value on metaclass"
meta_non_data_descriptor1: NonDataDescriptor = NonDataDescriptor()
meta_non_data_descriptor2: NonDataDescriptor = NonDataDescriptor()
class C3(metaclass=Meta3):
meta_attribute1: Literal["value on class"] = "value on class"
meta_attribute2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
meta_non_data_descriptor1: Literal["value on class"] = "value on class"
meta_non_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
reveal_type(C3.meta_attribute1) # revealed: Literal["value on class"]
reveal_type(C3.meta_attribute2) # revealed: Literal["class level data descriptor"]
reveal_type(C3.meta_non_data_descriptor1) # revealed: Literal["value on class"]
reveal_type(C3.meta_non_data_descriptor2) # revealed: Literal["class level data descriptor"]
```
Finally, metaclass attributes and metaclass non-data descriptors are only accessible when they are
not shadowed by class-level attributes:
```py
class Meta4(type):
meta_attribute: Literal["value on metaclass"] = "value on metaclass"
meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
class C4(metaclass=Meta4): ...
reveal_type(C4.meta_attribute) # revealed: Literal["value on metaclass"]
reveal_type(C4.meta_non_data_descriptor) # revealed: Literal["non-data"]
```
When a metaclass data descriptor is possibly unbound, we union the result type of its `__get__`
method with an underlying class level attribute, if present:
```py
def _(flag: bool):
class Meta5(type):
if flag:
meta_data_descriptor1: DataDescriptor = DataDescriptor()
meta_data_descriptor2: DataDescriptor = DataDescriptor()
class C5(metaclass=Meta5):
meta_data_descriptor1: Literal["value on class"] = "value on class"
reveal_type(C5.meta_data_descriptor1) # revealed: Literal["data", "value on class"]
# error: [possibly-unbound-attribute]
reveal_type(C5.meta_data_descriptor2) # revealed: Literal["data"]
```
When a class-level attribute is possibly unbound, we union its (descriptor protocol) type with the
metaclass attribute (unless it's a data descriptor, which always takes precedence):
```py
from typing import Any
def _(flag: bool):
class Meta6(type):
attribute1: DataDescriptor = DataDescriptor()
attribute2: NonDataDescriptor = NonDataDescriptor()
attribute3: Literal["value on metaclass"] = "value on metaclass"
class C6(metaclass=Meta6):
if flag:
attribute1: Literal["value on class"] = "value on class"
attribute2: Literal["value on class"] = "value on class"
attribute3: Literal["value on class"] = "value on class"
attribute4: Literal["value on class"] = "value on class"
reveal_type(C6.attribute1) # revealed: Literal["data"]
reveal_type(C6.attribute2) # revealed: Literal["non-data", "value on class"]
reveal_type(C6.attribute3) # revealed: Literal["value on metaclass", "value on class"]
# error: [possibly-unbound-attribute]
reveal_type(C6.attribute4) # revealed: Literal["value on class"]
```
Finally, we can also have unions of various types of attributes:
```py
def _(flag: bool):
class Meta7(type):
if flag:
union_of_metaclass_attributes: Literal[1] = 1
union_of_metaclass_data_descriptor_and_attribute: DataDescriptor = DataDescriptor()
else:
union_of_metaclass_attributes: Literal[2] = 2
union_of_metaclass_data_descriptor_and_attribute: Literal[2] = 2
class C7(metaclass=Meta7):
if flag:
union_of_class_attributes: Literal[1] = 1
union_of_class_data_descriptor_and_attribute: DataDescriptor = DataDescriptor()
else:
union_of_class_attributes: Literal[2] = 2
union_of_class_data_descriptor_and_attribute: Literal[2] = 2
reveal_type(C7.union_of_metaclass_attributes) # revealed: Literal[1, 2]
reveal_type(C7.union_of_metaclass_data_descriptor_and_attribute) # revealed: Literal["data", 2]
reveal_type(C7.union_of_class_attributes) # revealed: Literal[1, 2]
reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
```
## Partial fall back
Our implementation of the descriptor protocol takes into account that symbols can be possibly
unbound. In those cases, we fall back to lower precedence steps of the descriptor protocol and union
all possible results accordingly. We start by defining a data and a non-data descriptor:
```py
from typing import Literal
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
return "data"
def __set__(self, instance: object, value: int) -> None:
pass
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
return "non-data"
```
Then, we demonstrate that we fall back to an instance attribute if a data descriptor is possibly
unbound:
```py
def f1(flag: bool):
class C1:
if flag:
attr = DataDescriptor()
def f(self):
self.attr = "normal"
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
```
We never treat implicit instance attributes as definitely bound, so we fall back to the non-data
descriptor here:
```py
def f2(flag: bool):
class C2:
def f(self):
self.attr = "normal"
attr = NonDataDescriptor()
reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"]
```
## Built-in `property` descriptor
The built-in `property` decorator creates a descriptor. The names for attribute reads/writes are
@@ -166,18 +378,21 @@ c = C()
reveal_type(c._name) # revealed: str | None
# Should be `str`
reveal_type(c.name) # revealed: @Todo(decorated method)
# TODO: Should be `str`
reveal_type(c.name) # revealed: <bound method `name` of `C`>
# Should be `builtins.property`
reveal_type(C.name) # revealed: Literal[name]
# This is fine:
# TODO: These should not emit errors
# error: [invalid-assignment]
c.name = "new"
# error: [invalid-assignment]
c.name = None
# TODO: this should be an error
# TODO: this should be an error, but with a proper error message
# error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to attribute `name` of type `<bound method `name` of `C`>`"
c.name = 42
```
@@ -225,8 +440,7 @@ class C:
def __init__(self):
self.ten: Ten = Ten()
# TODO: Should be Ten
reveal_type(C().ten) # revealed: Literal[10]
reveal_type(C().ten) # revealed: Ten
```
## Descriptors distinguishing between class and instance access
@@ -295,12 +509,20 @@ class TailoredForInstanceAccess:
def __get__(self, instance: C, owner: type[C] | None = None) -> str:
return "a"
class C:
class TailoredForMetaclassAccess:
def __get__(self, instance: type[C], owner: type[Meta]) -> bytes:
return b"a"
class Meta(type):
metaclass_access: TailoredForMetaclassAccess = TailoredForMetaclassAccess()
class C(metaclass=Meta):
class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess()
instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess()
reveal_type(C.class_object_access) # revealed: int
reveal_type(C().instance_access) # revealed: str
reveal_type(C.metaclass_access) # revealed: bytes
# TODO: These should emit a diagnostic
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
@@ -320,6 +542,42 @@ class C:
# TODO: This should be an error
reveal_type(C.descriptor) # revealed: Descriptor
# TODO: This should be an error
reveal_type(C().descriptor) # revealed: Descriptor
```
## Possibly unbound descriptor attributes
```py
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __set__(self, instance: int, value) -> None:
pass
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def _(flag: bool):
class PossiblyUnbound:
if flag:
non_data: NonDataDescriptor = NonDataDescriptor()
data: DataDescriptor = DataDescriptor()
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound"
reveal_type(PossiblyUnbound.non_data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
reveal_type(PossiblyUnbound().non_data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound"
reveal_type(PossiblyUnbound.data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
reveal_type(PossiblyUnbound().data) # revealed: int
```
## Possibly-unbound `__get__` method
@@ -334,13 +592,55 @@ def _(flag: bool):
class C:
descriptor: MaybeDescriptor = MaybeDescriptor()
# TODO: This should be `MaybeDescriptor | int`
reveal_type(C.descriptor) # revealed: int
reveal_type(C.descriptor) # revealed: int | MaybeDescriptor
reveal_type(C().descriptor) # revealed: int | MaybeDescriptor
```
## Descriptors with non-function `__get__` callables that are descriptors themselves
The descriptor protocol is recursive, i.e. looking up `__get__` can involve triggering the
descriptor protocol on the callable's `__call__` method:
```py
from __future__ import annotations
class ReturnedCallable2:
def __call__(self, descriptor: Descriptor1, instance: None, owner: type[C]) -> int:
return 1
class ReturnedCallable1:
def __call__(self, descriptor: Descriptor2, instance: Callable1, owner: type[Callable1]) -> ReturnedCallable2:
return ReturnedCallable2()
class Callable3:
def __call__(self, descriptor: Descriptor3, instance: Callable2, owner: type[Callable2]) -> ReturnedCallable1:
return ReturnedCallable1()
class Descriptor3:
__get__: Callable3 = Callable3()
class Callable2:
__call__: Descriptor3 = Descriptor3()
class Descriptor2:
__get__: Callable2 = Callable2()
class Callable1:
__call__: Descriptor2 = Descriptor2()
class Descriptor1:
__get__: Callable1 = Callable1()
class C:
d: Descriptor1 = Descriptor1()
reveal_type(C.d) # revealed: int
```
## Dunder methods
Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol:
Dunder methods are looked up on the meta-type, but we still need to invoke the descriptor protocol:
```py
class SomeCallable:
@@ -438,4 +738,5 @@ wrapper_descriptor(f, None, type(f), "one too many")
```
[descriptors]: https://docs.python.org/3/howto/descriptor.html
[precedence chain]: https://github.com/python/cpython/blob/3.13/Objects/typeobject.c#L5393-L5481
[simple example]: https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant

View File

@@ -0,0 +1,100 @@
# `lambda` expression
## No parameters
`lambda` expressions can be defined without any parameters.
```py
reveal_type(lambda: 1) # revealed: () -> @Todo(lambda return type)
# error: [unresolved-reference]
reveal_type(lambda: a) # revealed: () -> @Todo(lambda return type)
```
## With parameters
Unlike parameters in function definition, the parameters in a `lambda` expression cannot be
annotated.
```py
reveal_type(lambda a: a) # revealed: (a) -> @Todo(lambda return type)
reveal_type(lambda a, b: a + b) # revealed: (a, b) -> @Todo(lambda return type)
```
But, it can have default values:
```py
reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> @Todo(lambda return type)
reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> @Todo(lambda return type)
```
And, positional-only parameters:
```py
reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> @Todo(lambda return type)
```
And, keyword-only parameters:
```py
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> @Todo(lambda return type)
```
And, variadic parameter:
```py
reveal_type(lambda *args: args) # revealed: (*args) -> @Todo(lambda return type)
```
And, keyword-varidic parameter:
```py
reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> @Todo(lambda return type)
```
Mixing all of them together:
```py
# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> @Todo(lambda return type)
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
```
## Parameter type
In addition to correctly inferring the `lambda` expression, the parameters should also be inferred
correctly.
Using a parameter with no default value:
```py
lambda x: reveal_type(x) # revealed: Unknown
```
Using a parameter with default value:
```py
lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1]
```
Using a variadic paramter:
```py
# TODO: should be `tuple[Unknown, ...]` (needs generics)
lambda *args: reveal_type(args) # revealed: tuple
```
Using a keyword-varidic parameter:
```py
# TODO: should be `dict[str, Unknown]` (needs generics)
lambda **kwargs: reveal_type(kwargs) # revealed: dict
```
## Nested `lambda` expressions
Here, a `lambda` expression is used as the default value for a parameter in another `lambda`
expression.
```py
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> @Todo(lambda return type)) -> @Todo(lambda return type)
```

View File

@@ -68,7 +68,7 @@ class C[T]:
# TODO: no error
# TODO: revealed: C[int]
# error: [non-subscriptable]
reveal_type(C[int]()) # revealed: Unknown
reveal_type(C[int]()) # revealed: C
```
We can infer the type parameter from a type context:
@@ -129,18 +129,19 @@ propagate through:
```py
class Base[T]:
x: T
x: T | None = None
# TODO: no error
# error: [non-subscriptable]
class Sub[U](Base[U]): ...
# TODO: no error
# TODO: revealed: int
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Base[int].x) # revealed: Unknown
# TODO: revealed: int
reveal_type(Sub[int].x) # revealed: Unknown
reveal_type(Base[int].x) # revealed: T | None
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Sub[int].x) # revealed: T | None
```
## Cyclic class definition

View File

@@ -216,9 +216,10 @@ from typing import Iterable
def f[T](x: T, y: T) -> None:
class Ok[S]: ...
# TODO: error
# TODO: error for reuse of typevar
class Bad1[T]: ...
# TODO: error
# TODO: no non-subscriptable error, error for reuse of typevar
# error: [non-subscriptable]
class Bad2(Iterable[T]): ...
```
@@ -229,9 +230,10 @@ from typing import Iterable
class C[T]:
class Ok1[S]: ...
# TODO: error
# TODO: error for reuse of typevar
class Bad1[T]: ...
# TODO: error
# TODO: no non-subscriptable error, error for reuse of typevar
# error: [non-subscriptable]
class Bad2(Iterable[T]): ...
```

View File

@@ -91,3 +91,16 @@ match while:
for x in foo.pass:
pass
```
## Invalid annotation
### `typing.Callable`
```py
from typing import Callable
# error: [invalid-syntax] "Expected index or slice expression"
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[]):
reveal_type(c) # revealed: (...) -> Unknown
```

View File

@@ -163,7 +163,7 @@ reveal_type(B.__class__) # revealed: Literal[M]
## Non-class
When a class has an explicit `metaclass` that is not a class, but is a callable that accepts
`type.__new__` arguments, we should return the meta type of its return type.
`type.__new__` arguments, we should return the meta-type of its return type.
```py
def f(*args, **kwargs) -> int: ...

View File

@@ -9,7 +9,7 @@ is unbound.
```py
reveal_type(__name__) # revealed: str
reveal_type(__file__) # revealed: str | None
reveal_type(__loader__) # revealed: @Todo(instance attribute on class with dynamic base) | None
reveal_type(__loader__) # revealed: LoaderProtocol | None
reveal_type(__package__) # revealed: str | None
reveal_type(__doc__) # revealed: str | None
@@ -151,6 +151,7 @@ typeshed = "/typeshed"
`/typeshed/stdlib/builtins.pyi`:
```pyi
class object: ...
class int: ...
class bytes: ...

View File

@@ -13,7 +13,8 @@ class Foo[T]: ...
class Bar(Foo[Bar]): ...
reveal_type(Bar) # revealed: Literal[Bar]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
# TODO: Instead of `Literal[Foo]`, we might eventually want to show a type that involves the type parameter.
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Literal[object]]
```
## Access to attributes declarated in stubs

View File

@@ -117,7 +117,6 @@ from typing import Tuple
class C(Tuple): ...
# Runtime value: `(C, tuple, typing.Generic, object)`
# TODO: Add `Generic` to the MRO
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[tuple], Unknown, Literal[object]]
# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
reveal_type(C.__mro__)
```

View File

@@ -38,16 +38,15 @@ For example, the `type: ignore` comment in this example suppresses the error of
`"test"` and adding `"other"` to the result of the cast.
```py
# fmt: off
from typing import cast
y = (
cast(int, "test" +
# TODO: Remove the expected error after implementing `invalid-operator` for binary expressions
# error: [unused-ignore-comment]
2 # type: ignore
# error: [unsupported-operator]
cast(
int,
2 + "test", # type: ignore
)
+ "other" # TODO: expected-error[invalid-operator]
+ "other"
)
```

View File

@@ -3,7 +3,7 @@
A type is single-valued iff it is not empty and all inhabitants of it compare equal.
```py
from typing_extensions import Any, Literal, LiteralString, Never
from typing_extensions import Any, Literal, LiteralString, Never, Callable
from knot_extensions import is_single_valued, static_assert
static_assert(is_single_valued(None))
@@ -22,4 +22,7 @@ static_assert(not is_single_valued(Any))
static_assert(not is_single_valued(Literal[1, 2]))
static_assert(not is_single_valued(tuple[None, int]))
static_assert(not is_single_valued(Callable[..., None]))
static_assert(not is_single_valued(Callable[[int, str], None]))
```

View File

@@ -5,7 +5,7 @@ A type is a singleton type iff it has exactly one inhabitant.
## Basic
```py
from typing_extensions import Literal, Never
from typing_extensions import Literal, Never, Callable
from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(None))
@@ -23,6 +23,9 @@ static_assert(not is_singleton(Literal[1, 2]))
static_assert(not is_singleton(tuple[()]))
static_assert(not is_singleton(tuple[None]))
static_assert(not is_singleton(tuple[None, Literal[True]]))
static_assert(not is_singleton(Callable[..., None]))
static_assert(not is_singleton(Callable[[int, str], None]))
```
## `NoDefault`

View File

@@ -383,7 +383,7 @@ static_assert(is_subtype_of(LiteralStr, type[object]))
static_assert(not is_subtype_of(type[str], LiteralStr))
# custom meta classes
# custom metaclasses
type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass]

View File

@@ -1,5 +1,9 @@
# Unpacking
If there are not enough or too many values when unpacking, an error will occur and the types of
all variables (if nested tuple unpacking fails, only the variables within the failed tuples) is
inferred to be `Unknown`.
## Tuple
### Simple tuple
@@ -63,8 +67,8 @@ reveal_type(c) # revealed: Literal[4]
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
(a, b, c) = (1, 2)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[2]
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
@@ -73,8 +77,30 @@ reveal_type(c) # revealed: Unknown
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
(a, b) = (1, 2, 3)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
### Nested uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
(a, (b, c), d) = (1, (2,), 3)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[2]
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Literal[3]
```
### Nested uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
(a, (b, c), d) = (1, (2, 3, 4), 5)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Literal[5]
```
### Starred expression (1)
@@ -82,10 +108,10 @@ reveal_type(b) # revealed: Literal[2]
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Literal[1]
reveal_type(a) # revealed: Unknown
# TODO: Should be list[Any] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[2]
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
```
@@ -135,10 +161,10 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)"
(a, b, c, *d, e, f) = (1,)
reveal_type(a) # revealed: Literal[1]
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: @Todo(starred unpacking)
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
```
@@ -201,8 +227,8 @@ reveal_type(b) # revealed: LiteralString
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
a, b, c = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
@@ -211,8 +237,8 @@ reveal_type(c) # revealed: Unknown
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
a, b = "abc"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
### Starred expression (1)
@@ -220,10 +246,19 @@ reveal_type(b) # revealed: LiteralString
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
(a, *b, c, d) = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
```
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 1)"
(a, b, *c, d) = "a"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
```
@@ -274,7 +309,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
(a, b) = "é"
reveal_type(a) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
@@ -284,7 +319,7 @@ reveal_type(b) # revealed: Unknown
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
(a, b) = "\u9e6c"
reveal_type(a) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
@@ -294,7 +329,7 @@ reveal_type(b) # revealed: Unknown
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
(a, b) = "\U0010ffff"
reveal_type(a) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
@@ -388,8 +423,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)"
a, b = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: bytes | int
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
### Size mismatch (2)
@@ -399,8 +434,8 @@ def _(arg: tuple[int, bytes] | tuple[int, str]):
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
a, b, c = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: bytes | str
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
@@ -542,7 +577,7 @@ for a, b in ((1, 2), ("a", "b")):
# error: "Object of type `Literal[4]` is not iterable"
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
reveal_type(a) # revealed: Unknown | Literal[3, 5] | LiteralString
reveal_type(a) # revealed: Unknown | Literal[3, 5]
reveal_type(b) # revealed: Unknown | Literal["a", "b"]
```
@@ -578,3 +613,98 @@ def _(arg: tuple[tuple[int, str], Iterable]):
reveal_type(a) # revealed: int | bytes
reveal_type(b) # revealed: str | bytes
```
## With statement
Unpacking in a `with` statement.
### Same types
```py
class ContextManager:
def __enter__(self) -> tuple[int, int]:
return (1, 2)
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
with ContextManager() as (a, b):
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```
### Mixed types
```py
class ContextManager:
def __enter__(self) -> tuple[int, str]:
return (1, "a")
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
with ContextManager() as (a, b):
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
```
### Nested
```py
class ContextManager:
def __enter__(self) -> tuple[int, tuple[str, bytes]]:
return (1, ("a", b"bytes"))
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
with ContextManager() as (a, (b, c)):
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
reveal_type(c) # revealed: bytes
```
### Starred expression
```py
class ContextManager:
def __enter__(self) -> tuple[int, int, int]:
return (1, 2, 3)
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
with ContextManager() as (a, *b):
reveal_type(a) # revealed: int
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo(starred unpacking)
```
### Unbound context manager expression
```py
# TODO: should only be one diagnostic
# error: [unresolved-reference] "Name `nonexistant` used when not defined"
# error: [unresolved-reference] "Name `nonexistant` used when not defined"
# error: [unresolved-reference] "Name `nonexistant` used when not defined"
with nonexistant as (x, y):
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: Unknown
```
### Invalid unpacking
```py
class ContextManager:
def __enter__(self) -> tuple[int, str]:
return (1, "a")
def __exit__(self, *args) -> None:
pass
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
with ContextManager() as (a, b, c):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```

View File

@@ -45,7 +45,7 @@ def _(flag: bool):
```py
class Manager: ...
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
with Manager():
...
```
@@ -56,7 +56,7 @@ with Manager():
class Manager:
def __exit__(self, exc_tpe, exc_value, traceback): ...
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__enter__`"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__`"
with Manager():
...
```
@@ -67,7 +67,7 @@ with Manager():
class Manager:
def __enter__(self): ...
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__exit__`"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__exit__`"
with Manager():
...
```
@@ -113,8 +113,7 @@ def _(flag: bool):
class NotAContextManager: ...
context_expr = Manager1() if flag else NotAContextManager()
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound"
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound"
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound"
with context_expr as f:
reveal_type(f) # revealed: str
```

View File

@@ -45,7 +45,7 @@ pub struct AstNodeRef<T> {
#[allow(unsafe_code)]
impl<T> AstNodeRef<T> {
/// Creates a new `AstNodeRef` that reference `node`. The `parsed` is the [`ParsedModule`] to
/// Creates a new `AstNodeRef` that references `node`. The `parsed` is the [`ParsedModule`] to
/// which the `AstNodeRef` belongs.
///
/// ## Safety

View File

@@ -22,6 +22,10 @@ pub(crate) enum AttributeAssignment<'db> {
/// `for self.x in <iterable>`.
Iterable { iterable: Expression<'db> },
/// An attribute assignment where the expression to be assigned is a context manager, for example
/// `with <context_manager> as self.x`.
ContextManager { context_manager: Expression<'db> },
/// An attribute assignment where the left-hand side is an unpacking expression,
/// e.g. `self.x, self.y = <value>`.
Unpack {

View File

@@ -1032,6 +1032,7 @@ where
self.db,
self.file,
self.current_scope(),
// SAFETY: `target` belongs to the `self.module` tree
#[allow(unsafe_code)]
unsafe {
AstNodeRef::new(self.module.clone(), target)
@@ -1262,16 +1263,64 @@ where
is_async,
..
}) => {
for item in items {
self.visit_expr(&item.context_expr);
if let Some(optional_vars) = item.optional_vars.as_deref() {
self.add_standalone_expression(&item.context_expr);
self.push_assignment(CurrentAssignment::WithItem {
item,
is_async: *is_async,
});
for item @ ruff_python_ast::WithItem {
range: _,
context_expr,
optional_vars,
} in items
{
self.visit_expr(context_expr);
if let Some(optional_vars) = optional_vars.as_deref() {
let context_manager = self.add_standalone_expression(context_expr);
let current_assignment = match optional_vars {
ast::Expr::Tuple(_) | ast::Expr::List(_) => {
Some(CurrentAssignment::WithItem {
item,
first: true,
is_async: *is_async,
unpack: Some(Unpack::new(
self.db,
self.file,
self.current_scope(),
// SAFETY: the node `optional_vars` belongs to the `self.module` tree
#[allow(unsafe_code)]
unsafe {
AstNodeRef::new(self.module.clone(), optional_vars)
},
UnpackValue::ContextManager(context_manager),
countme::Count::default(),
)),
})
}
ast::Expr::Name(_) => Some(CurrentAssignment::WithItem {
item,
is_async: *is_async,
unpack: None,
// `false` is arbitrary here---we don't actually use it other than in the actual unpacks
first: false,
}),
ast::Expr::Attribute(ast::ExprAttribute {
value: object,
attr,
..
}) => {
self.register_attribute_assignment(
object,
attr,
AttributeAssignment::ContextManager { context_manager },
);
None
}
_ => None,
};
if let Some(current_assignment) = current_assignment {
self.push_assignment(current_assignment);
}
self.visit_expr(optional_vars);
self.pop_assignment();
if current_assignment.is_some() {
self.pop_assignment();
}
}
}
self.visit_body(body);
@@ -1304,6 +1353,7 @@ where
self.db,
self.file,
self.current_scope(),
// SAFETY: the node `target` belongs to the `self.module` tree
#[allow(unsafe_code)]
unsafe {
AstNodeRef::new(self.module.clone(), target)
@@ -1631,12 +1681,19 @@ where
},
);
}
Some(CurrentAssignment::WithItem { item, is_async }) => {
Some(CurrentAssignment::WithItem {
item,
first,
is_async,
unpack,
}) => {
self.add_definition(
symbol,
WithItemDefinitionNodeRef {
node: item,
target: name_node,
unpack,
context_expr: &item.context_expr,
name: name_node,
first,
is_async,
},
);
@@ -1646,7 +1703,9 @@ where
}
if let Some(
CurrentAssignment::Assign { first, .. } | CurrentAssignment::For { first, .. },
CurrentAssignment::Assign { first, .. }
| CurrentAssignment::For { first, .. }
| CurrentAssignment::WithItem { first, .. },
) = self.current_assignment_mut()
{
*first = false;
@@ -1826,6 +1885,10 @@ where
| CurrentAssignment::For {
unpack: Some(unpack),
..
}
| CurrentAssignment::WithItem {
unpack: Some(unpack),
..
},
) = self.current_assignment()
{
@@ -1919,7 +1982,9 @@ enum CurrentAssignment<'a> {
},
WithItem {
item: &'a ast::WithItem,
first: bool,
is_async: bool,
unpack: Option<Unpack<'a>>,
},
}

View File

@@ -201,8 +201,10 @@ pub(crate) struct AssignmentDefinitionNodeRef<'a> {
#[derive(Copy, Clone, Debug)]
pub(crate) struct WithItemDefinitionNodeRef<'a> {
pub(crate) node: &'a ast::WithItem,
pub(crate) target: &'a ast::ExprName,
pub(crate) unpack: Option<Unpack<'a>>,
pub(crate) context_expr: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName,
pub(crate) first: bool,
pub(crate) is_async: bool,
}
@@ -323,12 +325,16 @@ impl<'db> DefinitionNodeRef<'db> {
DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter))
}
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef {
node,
target,
unpack,
context_expr,
name,
first,
is_async,
}) => DefinitionKind::WithItem(WithItemDefinitionKind {
node: AstNodeRef::new(parsed.clone(), node),
target: AstNodeRef::new(parsed, target),
target: TargetKind::from(unpack),
context_expr: AstNodeRef::new(parsed.clone(), context_expr),
name: AstNodeRef::new(parsed, name),
first,
is_async,
}),
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
@@ -394,10 +400,12 @@ impl<'db> DefinitionNodeRef<'db> {
Self::VariadicKeywordParameter(node) => node.into(),
Self::Parameter(node) => node.into(),
Self::WithItem(WithItemDefinitionNodeRef {
node: _,
target,
unpack: _,
context_expr: _,
first: _,
is_async: _,
}) => target.into(),
name,
}) => name.into(),
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
identifier.into()
}
@@ -467,7 +475,7 @@ pub enum DefinitionKind<'db> {
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
Parameter(AstNodeRef<ast::ParameterWithDefault>),
WithItem(WithItemDefinitionKind),
WithItem(WithItemDefinitionKind<'db>),
MatchPattern(MatchPatternDefinitionKind),
ExceptHandler(ExceptHandlerDefinitionKind),
TypeVar(AstNodeRef<ast::TypeParamTypeVar>),
@@ -506,7 +514,7 @@ impl DefinitionKind<'_> {
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(),
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(),
DefinitionKind::Parameter(parameter) => parameter.parameter.name.range(),
DefinitionKind::WithItem(with_item) => with_item.target().range(),
DefinitionKind::WithItem(with_item) => with_item.name().range(),
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
DefinitionKind::TypeVar(type_var) => type_var.name.range(),
@@ -688,19 +696,29 @@ impl<'db> AssignmentDefinitionKind<'db> {
}
#[derive(Clone, Debug)]
pub struct WithItemDefinitionKind {
node: AstNodeRef<ast::WithItem>,
target: AstNodeRef<ast::ExprName>,
pub struct WithItemDefinitionKind<'db> {
target: TargetKind<'db>,
context_expr: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
first: bool,
is_async: bool,
}
impl WithItemDefinitionKind {
pub(crate) fn node(&self) -> &ast::WithItem {
self.node.node()
impl<'db> WithItemDefinitionKind<'db> {
pub(crate) fn context_expr(&self) -> &ast::Expr {
self.context_expr.node()
}
pub(crate) fn target(&self) -> &ast::ExprName {
self.target.node()
pub(crate) fn target(&self) -> TargetKind<'db> {
self.target
}
pub(crate) fn name(&self) -> &ast::ExprName {
self.name.node()
}
pub(crate) const fn is_first(&self) -> bool {
self.first
}
pub(crate) const fn is_async(&self) -> bool {

View File

@@ -6,8 +6,8 @@ use hashbrown::hash_map::RawEntryMut;
use ruff_db::files::File;
use ruff_db::parsed::ParsedModule;
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast};
use rustc_hash::FxHasher;
use crate::ast_node_ref::AstNodeRef;

View File

@@ -21,6 +21,15 @@ pub(crate) enum Boundness {
PossiblyUnbound,
}
impl Boundness {
pub(crate) const fn max(self, other: Self) -> Self {
match (self, other) {
(Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound,
(Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound,
}
}
}
/// The result of a symbol lookup, which can either be a (possibly unbound) type
/// or a completely unbound symbol.
///
@@ -79,51 +88,6 @@ impl<'db> Symbol<'db> {
.expect("Expected a (possibly unbound) type, not an unbound symbol")
}
/// Transform the symbol into a [`LookupResult`],
/// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
pub(crate) fn into_lookup_result(self) -> LookupResult<'db> {
match self {
Symbol::Type(ty, Boundness::Bound) => Ok(ty),
Symbol::Type(ty, Boundness::PossiblyUnbound) => Err(LookupError::PossiblyUnbound(ty)),
Symbol::Unbound => Err(LookupError::Unbound),
}
}
/// Safely unwrap the symbol into a [`Type`].
///
/// If the symbol is definitely unbound or possibly unbound, it will be transformed into a
/// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning
/// the result of `diagnostic_fn` (which will be a [`Type`]). This allows the caller to ensure
/// that a diagnostic is emitted if the symbol is possibly or definitely unbound.
pub(crate) fn unwrap_with_diagnostic(
self,
diagnostic_fn: impl FnOnce(LookupError<'db>) -> Type<'db>,
) -> Type<'db> {
self.into_lookup_result().unwrap_or_else(diagnostic_fn)
}
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
///
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
/// 2. Else, evaluate `fallback_fn()`:
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
/// b. Else, if `fallback` is definitely unbound, return `self`.
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
#[must_use]
pub(crate) fn or_fall_back_to(
self,
db: &'db dyn Db,
fallback_fn: impl FnOnce() -> Self,
) -> Self {
self.into_lookup_result()
.or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn()))
.into()
}
#[must_use]
pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Symbol<'db> {
match self {
@@ -131,14 +95,28 @@ impl<'db> Symbol<'db> {
Symbol::Unbound => Symbol::Unbound,
}
}
#[must_use]
pub(crate) fn with_qualifiers(self, qualifiers: TypeQualifiers) -> SymbolAndQualifiers<'db> {
SymbolAndQualifiers {
symbol: self,
qualifiers,
}
}
}
impl<'db> From<LookupResult<'db>> for Symbol<'db> {
impl<'db> From<LookupResult<'db>> for SymbolAndQualifiers<'db> {
fn from(value: LookupResult<'db>) -> Self {
match value {
Ok(ty) => Symbol::Type(ty, Boundness::Bound),
Err(LookupError::Unbound) => Symbol::Unbound,
Err(LookupError::PossiblyUnbound(ty)) => Symbol::Type(ty, Boundness::PossiblyUnbound),
Ok(type_and_qualifiers) => {
Symbol::Type(type_and_qualifiers.inner_type(), Boundness::Bound)
.with_qualifiers(type_and_qualifiers.qualifiers())
}
Err(LookupError::Unbound(qualifiers)) => Symbol::Unbound.with_qualifiers(qualifiers),
Err(LookupError::PossiblyUnbound(type_and_qualifiers)) => {
Symbol::Type(type_and_qualifiers.inner_type(), Boundness::PossiblyUnbound)
.with_qualifiers(type_and_qualifiers.qualifiers())
}
}
}
}
@@ -146,8 +124,8 @@ impl<'db> From<LookupResult<'db>> for Symbol<'db> {
/// Possible ways in which a symbol lookup can (possibly or definitely) fail.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum LookupError<'db> {
Unbound,
PossiblyUnbound(Type<'db>),
Unbound(TypeQualifiers),
PossiblyUnbound(TypeAndQualifiers<'db>),
}
impl<'db> LookupError<'db> {
@@ -155,18 +133,22 @@ impl<'db> LookupError<'db> {
pub(crate) fn or_fall_back_to(
self,
db: &'db dyn Db,
fallback: Symbol<'db>,
fallback: SymbolAndQualifiers<'db>,
) -> LookupResult<'db> {
let fallback = fallback.into_lookup_result();
match (&self, &fallback) {
(LookupError::Unbound, _) => fallback,
(LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound)) => Err(self),
(LookupError::PossiblyUnbound(ty), Ok(ty2)) => {
Ok(UnionType::from_elements(db, [ty, ty2]))
(LookupError::Unbound(_), _) => fallback,
(LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound(_))) => Err(self),
(LookupError::PossiblyUnbound(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new(
UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]),
ty.qualifiers().union(ty2.qualifiers()),
)),
(LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => {
Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new(
UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]),
ty.qualifiers().union(ty2.qualifiers()),
)))
}
(LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => Err(
LookupError::PossiblyUnbound(UnionType::from_elements(db, [ty, ty2])),
),
}
}
}
@@ -176,17 +158,25 @@ impl<'db> LookupError<'db> {
///
/// Note that this type is exactly isomorphic to [`Symbol`].
/// In the future, we could possibly consider removing `Symbol` and using this type everywhere instead.
pub(crate) type LookupResult<'db> = Result<Type<'db>, LookupError<'db>>;
pub(crate) type LookupResult<'db> = Result<TypeAndQualifiers<'db>, LookupError<'db>>;
/// Infer the public type of a symbol (its type as seen from outside its scope) in the given
/// `scope`.
pub(crate) fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
pub(crate) fn symbol<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
name: &str,
) -> SymbolAndQualifiers<'db> {
symbol_impl(db, scope, name, RequiresExplicitReExport::No)
}
/// Infer the public type of a class symbol (its type as seen from outside its scope) in the given
/// `scope`.
pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
pub(crate) fn class_symbol<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
name: &str,
) -> SymbolAndQualifiers<'db> {
symbol_table(db, scope)
.symbol_id_by_name(name)
.map(|symbol| {
@@ -195,10 +185,14 @@ pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
if symbol_and_quals.is_class_var() {
// For declared class vars we do not need to check if they have bindings,
// we just trust the declaration.
return symbol_and_quals.0;
return symbol_and_quals;
}
if let SymbolAndQualifiers(Symbol::Type(ty, _), _) = symbol_and_quals {
if let SymbolAndQualifiers {
symbol: Symbol::Type(ty, _),
qualifiers,
} = symbol_and_quals
{
// Otherwise, we need to check if the symbol has bindings
let use_def = use_def_map(db, scope);
let bindings = use_def.public_bindings(symbol);
@@ -208,14 +202,16 @@ pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
// TODO: we should not need to calculate inferred type second time. This is a temporary
// solution until the notion of Boundness and Declaredness is split. See #16036, #16264
match inferred {
Symbol::Unbound => Symbol::Unbound,
Symbol::Type(_, boundness) => Symbol::Type(ty, boundness),
Symbol::Unbound => Symbol::Unbound.with_qualifiers(qualifiers),
Symbol::Type(_, boundness) => {
Symbol::Type(ty, boundness).with_qualifiers(qualifiers)
}
}
} else {
Symbol::Unbound
Symbol::Unbound.into()
}
})
.unwrap_or(Symbol::Unbound)
.unwrap_or_default()
}
/// Infers the public type of an explicit module-global symbol as seen from within the same file.
@@ -226,7 +222,11 @@ pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
/// those additional symbols.
///
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
pub(crate) fn explicit_global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
pub(crate) fn explicit_global_symbol<'db>(
db: &'db dyn Db,
file: File,
name: &str,
) -> SymbolAndQualifiers<'db> {
symbol_impl(
db,
global_scope(db, file),
@@ -243,13 +243,21 @@ pub(crate) fn explicit_global_symbol<'db>(db: &'db dyn Db, file: File, name: &st
///
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
#[cfg(test)]
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
pub(crate) fn global_symbol<'db>(
db: &'db dyn Db,
file: File,
name: &str,
) -> SymbolAndQualifiers<'db> {
explicit_global_symbol(db, file, name)
.or_fall_back_to(db, || module_type_implicit_global_symbol(db, name))
}
/// Infers the public type of an imported symbol.
pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str) -> Symbol<'db> {
pub(crate) fn imported_symbol<'db>(
db: &'db dyn Db,
module: &Module,
name: &str,
) -> SymbolAndQualifiers<'db> {
// If it's not found in the global scope, check if it's present as an instance on
// `types.ModuleType` or `builtins.object`.
//
@@ -267,7 +275,7 @@ pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str)
// module we're dealing with.
external_symbol_impl(db, module.file(), name).or_fall_back_to(db, || {
if name == "__getattr__" {
Symbol::Unbound
Symbol::Unbound.into()
} else {
KnownClass::ModuleType.to_instance(db).member(db, name)
}
@@ -281,7 +289,7 @@ pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str)
/// Note that this function is only intended for use in the context of the builtins *namespace*
/// and should not be used when a symbol is being explicitly imported from the `builtins` module
/// (e.g. `from builtins import int`).
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> {
resolve_module(db, &KnownModule::Builtins.name())
.map(|module| {
external_symbol_impl(db, module.file(), symbol).or_fall_back_to(db, || {
@@ -291,7 +299,7 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db>
module_type_implicit_global_symbol(db, symbol)
})
})
.unwrap_or(Symbol::Unbound)
.unwrap_or_default()
}
/// Lookup the type of `symbol` in a given known module.
@@ -301,10 +309,10 @@ pub(crate) fn known_module_symbol<'db>(
db: &'db dyn Db,
known_module: KnownModule,
symbol: &str,
) -> Symbol<'db> {
) -> SymbolAndQualifiers<'db> {
resolve_module(db, &known_module.name())
.map(|module| imported_symbol(db, &module, symbol))
.unwrap_or(Symbol::Unbound)
.unwrap_or_default()
}
/// Lookup the type of `symbol` in the `typing` module namespace.
@@ -312,7 +320,7 @@ pub(crate) fn known_module_symbol<'db>(
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.
#[inline]
#[cfg(test)]
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> {
known_module_symbol(db, KnownModule::Typing, symbol)
}
@@ -320,7 +328,10 @@ pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
///
/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason.
#[inline]
pub(crate) fn typing_extensions_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
pub(crate) fn typing_extensions_symbol<'db>(
db: &'db dyn Db,
symbol: &str,
) -> SymbolAndQualifiers<'db> {
known_module_symbol(db, KnownModule::TypingExtensions, symbol)
}
@@ -383,26 +394,97 @@ pub(crate) type SymbolFromDeclarationsResult<'db> =
///
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(crate) struct SymbolAndQualifiers<'db>(pub(crate) Symbol<'db>, pub(crate) TypeQualifiers);
pub(crate) struct SymbolAndQualifiers<'db> {
pub(crate) symbol: Symbol<'db>,
pub(crate) qualifiers: TypeQualifiers,
}
impl SymbolAndQualifiers<'_> {
impl Default for SymbolAndQualifiers<'_> {
fn default() -> Self {
SymbolAndQualifiers {
symbol: Symbol::Unbound,
qualifiers: TypeQualifiers::empty(),
}
}
}
impl<'db> SymbolAndQualifiers<'db> {
/// Constructor that creates a [`SymbolAndQualifiers`] instance with a [`TodoType`] type
/// and no qualifiers.
///
/// [`TodoType`]: crate::types::TodoType
pub(crate) fn todo(message: &'static str) -> Self {
Self(Symbol::todo(message), TypeQualifiers::empty())
Self {
symbol: Symbol::todo(message),
qualifiers: TypeQualifiers::empty(),
}
}
/// Returns `true` if the symbol has a `ClassVar` type qualifier.
pub(crate) fn is_class_var(&self) -> bool {
self.1.contains(TypeQualifiers::CLASS_VAR)
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
}
/// Transform symbol and qualifiers into a [`LookupResult`],
/// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
pub(crate) fn into_lookup_result(self) -> LookupResult<'db> {
match self {
SymbolAndQualifiers {
symbol: Symbol::Type(ty, Boundness::Bound),
qualifiers,
} => Ok(TypeAndQualifiers::new(ty, qualifiers)),
SymbolAndQualifiers {
symbol: Symbol::Type(ty, Boundness::PossiblyUnbound),
qualifiers,
} => Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new(
ty, qualifiers,
))),
SymbolAndQualifiers {
symbol: Symbol::Unbound,
qualifiers,
} => Err(LookupError::Unbound(qualifiers)),
}
}
/// Safely unwrap the symbol and the qualifiers into a [`TypeQualifiers`].
///
/// If the symbol is definitely unbound or possibly unbound, it will be transformed into a
/// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning
/// the result of `diagnostic_fn` (which will be a [`TypeQualifiers`]). This allows the caller
/// to ensure that a diagnostic is emitted if the symbol is possibly or definitely unbound.
pub(crate) fn unwrap_with_diagnostic(
self,
diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>,
) -> TypeAndQualifiers<'db> {
self.into_lookup_result().unwrap_or_else(diagnostic_fn)
}
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
///
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
/// 2. Else, evaluate `fallback_fn()`:
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
/// b. Else, if `fallback` is definitely unbound, return `self`.
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
#[must_use]
pub(crate) fn or_fall_back_to(
self,
db: &'db dyn Db,
fallback_fn: impl FnOnce() -> SymbolAndQualifiers<'db>,
) -> Self {
self.into_lookup_result()
.or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn()))
.into()
}
}
impl<'db> From<Symbol<'db>> for SymbolAndQualifiers<'db> {
fn from(symbol: Symbol<'db>) -> Self {
SymbolAndQualifiers(symbol, TypeQualifiers::empty())
symbol.with_qualifiers(TypeQualifiers::empty())
}
}
@@ -423,11 +505,17 @@ fn symbol_by_id<'db>(
match declared {
// Symbol is declared, trust the declared type
Ok(symbol_and_quals @ SymbolAndQualifiers(Symbol::Type(_, Boundness::Bound), _)) => {
symbol_and_quals
}
Ok(
symbol_and_quals @ SymbolAndQualifiers {
symbol: Symbol::Type(_, Boundness::Bound),
qualifiers: _,
},
) => symbol_and_quals,
// Symbol is possibly declared
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::PossiblyUnbound), quals)) => {
Ok(SymbolAndQualifiers {
symbol: Symbol::Type(declared_ty, Boundness::PossiblyUnbound),
qualifiers,
}) => {
let bindings = use_def.public_bindings(symbol_id);
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
@@ -446,10 +534,13 @@ fn symbol_by_id<'db>(
),
};
SymbolAndQualifiers(symbol, quals)
SymbolAndQualifiers { symbol, qualifiers }
}
// Symbol is undeclared, return the union of `Unknown` with the inferred type
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
Ok(SymbolAndQualifiers {
symbol: Symbol::Unbound,
qualifiers: _,
}) => {
let bindings = use_def.public_bindings(symbol_id);
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
@@ -471,13 +562,10 @@ fn symbol_by_id<'db>(
.into()
}
// Symbol has conflicting declared types
Err((declared_ty, _)) => {
Err((declared, _)) => {
// Intentionally ignore conflicting declared types; that's not our problem,
// it's the problem of the module we are importing from.
SymbolAndQualifiers(
Symbol::bound(declared_ty.inner_type()),
declared_ty.qualifiers(),
)
Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers())
}
}
@@ -503,7 +591,7 @@ fn symbol_impl<'db>(
scope: ScopeId<'db>,
name: &str,
requires_explicit_reexport: RequiresExplicitReExport,
) -> Symbol<'db> {
) -> SymbolAndQualifiers<'db> {
let _span = tracing::trace_span!("symbol", ?name).entered();
if name == "platform"
@@ -512,7 +600,7 @@ fn symbol_impl<'db>(
{
match Program::get(db).python_platform(db) {
crate::PythonPlatform::Identifier(platform) => {
return Symbol::bound(Type::string_literal(db, platform.as_str()));
return Symbol::bound(Type::string_literal(db, platform.as_str())).into();
}
crate::PythonPlatform::All => {
// Fall through to the looked up type
@@ -522,8 +610,8 @@ fn symbol_impl<'db>(
symbol_table(db, scope)
.symbol_id_by_name(name)
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport).0)
.unwrap_or(Symbol::Unbound)
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport))
.unwrap_or_default()
}
/// Implementation of [`symbol_from_bindings`].
@@ -669,7 +757,7 @@ fn symbol_from_declarations_impl<'db>(
if let Some(first) = types.next() {
let mut conflicting: Vec<Type<'db>> = vec![];
let declared_ty = if let Some(second) = types.next() {
let declared = if let Some(second) = types.next() {
let ty_first = first.inner_type();
let mut qualifiers = first.qualifiers();
@@ -695,13 +783,11 @@ fn symbol_from_declarations_impl<'db>(
Truthiness::Ambiguous => Boundness::PossiblyUnbound,
};
Ok(SymbolAndQualifiers(
Symbol::Type(declared_ty.inner_type(), boundness),
declared_ty.qualifiers(),
))
Ok(Symbol::Type(declared.inner_type(), boundness)
.with_qualifiers(declared.qualifiers()))
} else {
Err((
declared_ty,
declared,
std::iter::once(first.inner_type())
.chain(conflicting)
.collect(),
@@ -717,6 +803,7 @@ mod implicit_globals {
use crate::db::Db;
use crate::semantic_index::{self, symbol_table};
use crate::symbol::SymbolAndQualifiers;
use crate::types::KnownClass;
use super::Symbol;
@@ -738,7 +825,7 @@ mod implicit_globals {
pub(crate) fn module_type_implicit_global_symbol<'db>(
db: &'db dyn Db,
name: &str,
) -> Symbol<'db> {
) -> SymbolAndQualifiers<'db> {
// In general we wouldn't check to see whether a symbol exists on a class before doing the
// `.member()` call on the instance type -- we'd just do the `.member`() call on the instance
// type, since it has the same end result. The reason to only call `.member()` on `ModuleType`
@@ -750,7 +837,7 @@ mod implicit_globals {
{
KnownClass::ModuleType.to_instance(db).member(db, name)
} else {
Symbol::Unbound
Symbol::Unbound.into()
}
}
@@ -820,7 +907,7 @@ mod implicit_globals {
///
/// This will take into account whether the definition of the symbol is being explicitly
/// re-exported from a stub file or not.
fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> SymbolAndQualifiers<'db> {
symbol_impl(
db,
global_scope(db, file),
@@ -881,48 +968,45 @@ mod tests {
let ty1 = Type::IntLiteral(1);
let ty2 = Type::IntLiteral(2);
let unbound = || Symbol::Unbound.with_qualifiers(TypeQualifiers::empty());
let possibly_unbound_ty1 =
|| Symbol::Type(ty1, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty());
let possibly_unbound_ty2 =
|| Symbol::Type(ty2, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty());
let bound_ty1 = || Symbol::Type(ty1, Bound).with_qualifiers(TypeQualifiers::empty());
let bound_ty2 = || Symbol::Type(ty2, Bound).with_qualifiers(TypeQualifiers::empty());
// Start from an unbound symbol
assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound());
assert_eq!(
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Unbound),
Symbol::Unbound
);
assert_eq!(
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, PossiblyUnbound)),
Symbol::Type(ty1, PossiblyUnbound)
);
assert_eq!(
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, Bound)),
Symbol::Type(ty1, Bound)
unbound().or_fall_back_to(&db, possibly_unbound_ty1),
possibly_unbound_ty1()
);
assert_eq!(unbound().or_fall_back_to(&db, bound_ty1), bound_ty1());
// Start from a possibly unbound symbol
assert_eq!(
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Unbound),
Symbol::Type(ty1, PossiblyUnbound)
possibly_unbound_ty1().or_fall_back_to(&db, unbound),
possibly_unbound_ty1()
);
assert_eq!(
Symbol::Type(ty1, PossiblyUnbound)
.or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound)
possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2),
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound).into()
);
assert_eq!(
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound)
possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2),
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound).into()
);
// Start from a definitely bound symbol
assert_eq!(bound_ty1().or_fall_back_to(&db, unbound), bound_ty1());
assert_eq!(
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Unbound),
Symbol::Type(ty1, Bound)
);
assert_eq!(
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
Symbol::Type(ty1, Bound)
);
assert_eq!(
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
Symbol::Type(ty1, Bound)
bound_ty1().or_fall_back_to(&db, possibly_unbound_ty2),
bound_ty1()
);
assert_eq!(bound_ty1().or_fall_back_to(&db, bound_ty2), bound_ty1());
}
#[track_caller]
@@ -937,24 +1021,27 @@ mod tests {
#[test]
fn implicit_builtin_globals() {
let db = setup_db();
assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__"));
assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").symbol);
}
#[test]
fn implicit_typing_globals() {
let db = setup_db();
assert_bound_string_symbol(&db, typing_symbol(&db, "__name__"));
assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").symbol);
}
#[test]
fn implicit_typing_extensions_globals() {
let db = setup_db();
assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__"));
assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").symbol);
}
#[test]
fn implicit_sys_globals() {
let db = setup_db();
assert_bound_string_symbol(&db, known_module_symbol(&db, KnownModule::Sys, "__name__"));
assert_bound_string_symbol(
&db,
known_module_symbol(&db, KnownModule::Sys, "__name__").symbol,
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@ use crate::{
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
},
types::{
definition_expression_type, CallArguments, CallError, MetaclassCandidate, TupleType,
UnionBuilder, UnionCallError,
definition_expression_type, CallArguments, CallError, DynamicType, MetaclassCandidate,
TupleType, UnionBuilder, UnionCallError, UnionType,
},
Db, KnownModule, Program,
};
@@ -318,10 +318,10 @@ impl<'db> Class<'db> {
/// The member resolves to a member on the class itself or any of its proper superclasses.
///
/// TODO: Should this be made private...?
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
if name == "__mro__" {
let tuple_elements = self.iter_mro(db).map(Type::from);
return Symbol::bound(TupleType::from_elements(db, tuple_elements));
return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into();
}
// If we encounter a dynamic type in this class's MRO, we'll save that dynamic type
@@ -332,10 +332,16 @@ impl<'db> Class<'db> {
// from the non-dynamic members of the class's MRO.
let mut dynamic_type_to_intersect_with: Option<Type<'db>> = None;
let mut lookup_result: LookupResult<'db> = Err(LookupError::Unbound);
let mut lookup_result: LookupResult<'db> =
Err(LookupError::Unbound(TypeQualifiers::empty()));
for superclass in self.iter_mro(db) {
match superclass {
ClassBase::Dynamic(DynamicType::TodoProtocol) => {
// TODO: We currently skip `Protocol` when looking up class members, in order to
// avoid creating many dynamic types in our test suite that would otherwise
// result from looking up attributes on builtin types like `str`, `list`, `tuple`
}
ClassBase::Dynamic(_) => {
// Note: calling `Type::from(superclass).member()` would be incorrect here.
// What we'd really want is a `Type::Any.own_class_member()` method,
@@ -353,15 +359,33 @@ impl<'db> Class<'db> {
}
}
match (Symbol::from(lookup_result), dynamic_type_to_intersect_with) {
(symbol, None) => symbol,
(Symbol::Type(ty, _), Some(dynamic_type)) => Symbol::bound(
match (
SymbolAndQualifiers::from(lookup_result),
dynamic_type_to_intersect_with,
) {
(symbol_and_qualifiers, None) => symbol_and_qualifiers,
(
SymbolAndQualifiers {
symbol: Symbol::Type(ty, _),
qualifiers,
},
Some(dynamic_type),
) => Symbol::bound(
IntersectionBuilder::new(db)
.add_positive(ty)
.add_positive(dynamic_type)
.build(),
),
(Symbol::Unbound, Some(dynamic_type)) => Symbol::bound(dynamic_type),
)
.with_qualifiers(qualifiers),
(
SymbolAndQualifiers {
symbol: Symbol::Unbound,
qualifiers,
},
Some(dynamic_type),
) => Symbol::bound(dynamic_type).with_qualifiers(qualifiers),
}
}
@@ -371,7 +395,7 @@ impl<'db> Class<'db> {
/// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope
/// directly. Use [`Class::class_member`] if you require a method that will
/// traverse through the MRO until it finds the member.
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
let body_scope = self.body_scope(db);
class_symbol(db, body_scope, name)
}
@@ -388,17 +412,24 @@ impl<'db> Class<'db> {
for superclass in self.iter_mro(db) {
match superclass {
ClassBase::Dynamic(DynamicType::TodoProtocol) => {
// TODO: We currently skip `Protocol` when looking up instance members, in order to
// avoid creating many dynamic types in our test suite that would otherwise
// result from looking up attributes on builtin types like `str`, `list`, `tuple`
}
ClassBase::Dynamic(_) => {
return SymbolAndQualifiers::todo(
"instance attribute on class with dynamic base",
);
}
ClassBase::Class(class) => {
if let member @ SymbolAndQualifiers(Symbol::Type(ty, boundness), qualifiers) =
class.own_instance_member(db, name)
if let member @ SymbolAndQualifiers {
symbol: Symbol::Type(ty, boundness),
qualifiers,
} = class.own_instance_member(db, name)
{
// TODO: We could raise a diagnostic here if there are conflicting type qualifiers
union_qualifiers = union_qualifiers.union(qualifiers);
union_qualifiers |= qualifiers;
if boundness == Boundness::Bound {
if union.is_empty() {
@@ -406,10 +437,8 @@ impl<'db> Class<'db> {
return member;
}
return SymbolAndQualifiers(
Symbol::bound(union.add(ty).build()),
union_qualifiers,
);
return Symbol::bound(union.add(ty).build())
.with_qualifiers(union_qualifiers);
}
// If we see a possibly-unbound symbol, we need to keep looking
@@ -421,15 +450,13 @@ impl<'db> Class<'db> {
}
if union.is_empty() {
SymbolAndQualifiers(Symbol::Unbound, TypeQualifiers::empty())
Symbol::Unbound.with_qualifiers(TypeQualifiers::empty())
} else {
// If we have reached this point, we know that we have only seen possibly-unbound symbols.
// This means that the final result is still possibly-unbound.
SymbolAndQualifiers(
Symbol::Type(union.build(), Boundness::PossiblyUnbound),
union_qualifiers,
)
Symbol::Type(union.build(), Boundness::PossiblyUnbound)
.with_qualifiers(union_qualifiers)
}
}
@@ -439,31 +466,18 @@ impl<'db> Class<'db> {
db: &'db dyn Db,
class_body_scope: ScopeId<'db>,
name: &str,
inferred_from_class_body: &Symbol<'db>,
) -> Symbol<'db> {
) -> Option<Type<'db>> {
// If we do not see any declarations of an attribute, neither in the class body nor in
// any method, we build a union of `Unknown` with the inferred types of all bindings of
// that attribute. We include `Unknown` in that union to account for the fact that the
// attribute might be externally modified.
let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown());
let mut union_boundness = Boundness::Bound;
if let Symbol::Type(ty, boundness) = inferred_from_class_body {
union_of_inferred_types = union_of_inferred_types.add(*ty);
union_boundness = *boundness;
}
let attribute_assignments = attribute_assignments(db, class_body_scope);
let Some(attribute_assignments) = attribute_assignments
let attribute_assignments = attribute_assignments
.as_deref()
.and_then(|assignments| assignments.get(name))
else {
if inferred_from_class_body.is_unbound() {
return Symbol::Unbound;
}
return Symbol::Type(union_of_inferred_types.build(), union_boundness);
};
.and_then(|assignments| assignments.get(name))?;
for attribute_assignment in attribute_assignments {
match attribute_assignment {
@@ -477,7 +491,7 @@ impl<'db> Class<'db> {
let annotation_ty = infer_expression_type(db, *annotation);
// TODO: check if there are conflicting declarations
return Symbol::bound(annotation_ty);
return Some(annotation_ty);
}
AttributeAssignment::Unannotated { value } => {
// We found an un-annotated attribute assignment of the form:
@@ -499,6 +513,16 @@ impl<'db> Class<'db> {
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
AttributeAssignment::ContextManager { context_manager } => {
// We found an attribute assignment like:
//
// with <context_manager> as self.name:
let context_ty = infer_expression_type(db, *context_manager);
let inferred_ty = context_ty.enter(db);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
AttributeAssignment::Unpack {
attribute_expression_id,
unpack,
@@ -516,7 +540,7 @@ impl<'db> Class<'db> {
}
}
Symbol::Type(union_of_inferred_types.build(), union_boundness)
Some(union_of_inferred_types.build())
}
/// A helper function for `instance_member` that looks up the `name` attribute only on
@@ -533,55 +557,93 @@ impl<'db> Class<'db> {
let use_def = use_def_map(db, body_scope);
let declarations = use_def.public_declarations(symbol_id);
match symbol_from_declarations(db, declarations) {
Ok(SymbolAndQualifiers(declared @ Symbol::Type(declared_ty, _), qualifiers)) => {
let declared_and_qualifiers = symbol_from_declarations(db, declarations);
match declared_and_qualifiers {
Ok(SymbolAndQualifiers {
symbol: declared @ Symbol::Type(declared_ty, declaredness),
qualifiers,
}) => {
// The attribute is declared in the class body.
if let Some(function) = declared_ty.into_function_literal() {
// TODO: Eventually, we are going to process all decorators correctly. This is
// just a temporary heuristic to provide a broad categorization
if function.has_known_class_decorator(db, KnownClass::Classmethod)
&& function.decorators(db).len() == 1
{
SymbolAndQualifiers(declared, qualifiers)
} else if function.has_known_class_decorator(db, KnownClass::Property) {
SymbolAndQualifiers::todo("@property")
} else if function.has_known_function_decorator(db, KnownFunction::Overload)
{
SymbolAndQualifiers::todo("overloaded method")
} else if !function.decorators(db).is_empty() {
SymbolAndQualifiers::todo("decorated method")
} else {
SymbolAndQualifiers(declared, qualifiers)
}
} else {
SymbolAndQualifiers(declared, qualifiers)
}
}
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
// The attribute is not *declared* in the class body. It could still be declared
// in a method, and it could also be *bound* in the class body (and/or in a method).
let bindings = use_def.public_bindings(symbol_id);
let inferred = symbol_from_bindings(db, bindings);
let has_binding = !inferred.is_unbound();
Self::implicit_instance_attribute(db, body_scope, name, &inferred).into()
if has_binding {
// The attribute is declared and bound in the class body.
if let Some(implicit_ty) =
Self::implicit_instance_attribute(db, body_scope, name)
{
if declaredness == Boundness::Bound {
// If a symbol is definitely declared, and we see
// attribute assignments in methods of the class,
// we trust the declared type.
declared.with_qualifiers(qualifiers)
} else {
Symbol::Type(
UnionType::from_elements(db, [declared_ty, implicit_ty]),
declaredness,
)
.with_qualifiers(qualifiers)
}
} else {
// The symbol is declared and bound in the class body,
// but we did not find any attribute assignments in
// methods of the class. This means that the attribute
// has a class-level default value, but it would not be
// found in a `__dict__` lookup.
Symbol::Unbound.into()
}
} else {
// The attribute is declared but not bound in the class body.
// We take this as a sign that this is intended to be a pure
// instance attribute, and we trust the declared type, unless
// it is possibly-undeclared. In the latter case, we also
// union with the inferred type from attribute assignments.
if declaredness == Boundness::Bound {
declared.with_qualifiers(qualifiers)
} else {
if let Some(implicit_ty) =
Self::implicit_instance_attribute(db, body_scope, name)
{
Symbol::Type(
UnionType::from_elements(db, [declared_ty, implicit_ty]),
declaredness,
)
.with_qualifiers(qualifiers)
} else {
declared.with_qualifiers(qualifiers)
}
}
}
}
Err((declared_ty, _conflicting_declarations)) => {
Ok(SymbolAndQualifiers {
symbol: Symbol::Unbound,
qualifiers: _,
}) => {
// The attribute is not *declared* in the class body. It could still be declared/bound
// in a method.
Self::implicit_instance_attribute(db, body_scope, name)
.map_or(Symbol::Unbound, Symbol::bound)
.into()
}
Err((declared, _conflicting_declarations)) => {
// There are conflicting declarations for this attribute in the class body.
SymbolAndQualifiers(
Symbol::bound(declared_ty.inner_type()),
declared_ty.qualifiers(),
)
Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers())
}
}
} else {
// This attribute is neither declared nor bound in the class body.
// It could still be implicitly defined in a method.
Self::implicit_instance_attribute(db, body_scope, name, &Symbol::Unbound).into()
Self::implicit_instance_attribute(db, body_scope, name)
.map_or(Symbol::Unbound, Symbol::bound)
.into()
}
}
@@ -663,7 +725,7 @@ impl<'db> ClassLiteralType<'db> {
self.class.body_scope(db)
}
pub(super) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
self.class.class_member(db, name)
}
}
@@ -881,6 +943,7 @@ impl<'db> KnownClass {
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
known_module_symbol(db, self.canonical_module(db), self.as_str(db))
.symbol
.ignore_possibly_unbound()
.unwrap_or(Type::unknown())
}
@@ -896,6 +959,7 @@ impl<'db> KnownClass {
/// *and* `class` is a subclass of `other`.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: Class<'db>) -> bool {
known_module_symbol(db, self.canonical_module(db), self.as_str(db))
.symbol
.ignore_possibly_unbound()
.and_then(Type::into_class_literal)
.is_some_and(|ClassLiteralType { class }| class.is_subclass_of(db, other))
@@ -1203,6 +1267,8 @@ pub enum KnownInstanceType<'db> {
Deque,
/// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`)
OrderedDict,
/// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`)
Protocol,
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
Type,
/// A single instance of `typing.TypeVar`
@@ -1274,6 +1340,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Deque
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::ReadOnly
| Self::TypeAliasType(_)
| Self::Unknown
@@ -1318,6 +1385,7 @@ impl<'db> KnownInstanceType<'db> {
Self::Deque => "typing.Deque",
Self::ChainMap => "typing.ChainMap",
Self::OrderedDict => "typing.OrderedDict",
Self::Protocol => "typing.Protocol",
Self::ReadOnly => "typing.ReadOnly",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
@@ -1364,6 +1432,7 @@ impl<'db> KnownInstanceType<'db> {
Self::Deque => KnownClass::StdlibAlias,
Self::ChainMap => KnownClass::StdlibAlias,
Self::OrderedDict => KnownClass::StdlibAlias,
Self::Protocol => KnownClass::SpecialForm,
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
Self::TypeOf => KnownClass::SpecialForm,
@@ -1406,6 +1475,7 @@ impl<'db> KnownInstanceType<'db> {
"Counter" => Self::Counter,
"ChainMap" => Self::ChainMap,
"OrderedDict" => Self::OrderedDict,
"Protocol" => Self::Protocol,
"Optional" => Self::Optional,
"Union" => Self::Union,
"NoReturn" => Self::NoReturn,
@@ -1457,6 +1527,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Counter
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::Optional
| Self::Union
| Self::NoReturn
@@ -1489,15 +1560,6 @@ impl<'db> KnownInstanceType<'db> {
| Self::TypeOf => module.is_knot_extensions(),
}
}
pub(super) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
let ty = match (self, name) {
(Self::TypeVar(typevar), "__name__") => Type::string_literal(db, typevar.name(db)),
(Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)),
_ => return self.instance_fallback(db).static_member(db, name),
};
Symbol::bound(ty)
}
}
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]

View File

@@ -144,6 +144,7 @@ impl<'db> ClassBase<'db> {
KnownInstanceType::Callable => {
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
}
KnownInstanceType::Protocol => Some(ClassBase::Dynamic(DynamicType::TodoProtocol)),
},
}
}

View File

@@ -1151,3 +1151,18 @@ pub(crate) fn report_invalid_arguments_to_annotated<'db>(
),
);
}
pub(crate) fn report_invalid_arguments_to_callable<'db>(
db: &'db dyn Db,
context: &InferContext<'db>,
subscript: &ast::ExprSubscript,
) {
context.report_lint(
&INVALID_TYPE_FORM,
subscript,
format_args!(
"Special form `{}` expected exactly two arguments (parameter types and return type)",
KnownInstanceType::Callable.repr(db)
),
);
}

View File

@@ -7,6 +7,7 @@ use ruff_python_ast::str::{Quote, TripleQuotes};
use ruff_python_literal::escape::AsciiEscape;
use crate::types::class_base::ClassBase;
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{
CallableType, ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
Type, UnionType,
@@ -88,6 +89,9 @@ impl Display for DisplayRepresentation<'_> {
},
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Callable(CallableType::General(callable)) => {
callable.signature(self.db).display(self.db).fmt(f)
}
Type::Callable(CallableType::BoundMethod(bound_method)) => {
write!(
f,
@@ -156,6 +160,99 @@ impl Display for DisplayRepresentation<'_> {
}
}
impl<'db> Signature<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplaySignature<'db> {
DisplaySignature {
parameters: self.parameters(),
return_ty: self.return_ty.as_ref(),
db,
}
}
}
struct DisplaySignature<'db> {
parameters: &'db Parameters<'db>,
return_ty: Option<&'db Type<'db>>,
db: &'db dyn Db,
}
impl Display for DisplaySignature<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_char('(')?;
if self.parameters.is_gradual() {
// We represent gradual form as `...` in the signature, internally the parameters still
// contain `(*args, **kwargs)` parameters.
f.write_str("...")?;
} else {
let mut star_added = false;
let mut needs_slash = false;
let mut join = f.join(", ");
for parameter in self.parameters.as_slice() {
if !star_added && parameter.is_keyword_only() {
join.entry(&'*');
star_added = true;
}
if parameter.is_positional_only() {
needs_slash = true;
} else if needs_slash {
join.entry(&'/');
needs_slash = false;
}
join.entry(&parameter.display(self.db));
}
if needs_slash {
join.entry(&'/');
}
join.finish()?;
}
write!(
f,
") -> {}",
self.return_ty.unwrap_or(&Type::unknown()).display(self.db)
)?;
Ok(())
}
}
impl<'db> Parameter<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayParameter<'db> {
DisplayParameter { param: self, db }
}
}
struct DisplayParameter<'db> {
param: &'db Parameter<'db>,
db: &'db dyn Db,
}
impl Display for DisplayParameter<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(name) = self.param.display_name() {
write!(f, "{name}")?;
if let Some(annotated_type) = self.param.annotated_type() {
write!(f, ": {}", annotated_type.display(self.db))?;
}
// Default value can only be specified if `name` is given.
if let Some(default_ty) = self.param.default_type() {
if self.param.annotated_type().is_some() {
write!(f, " = {}", default_ty.display(self.db))?;
} else {
write!(f, "={}", default_ty.display(self.db))?;
}
}
} else if let Some(ty) = self.param.annotated_type() {
// This case is specifically for the `Callable` signature where name and default value
// cannot be provided.
ty.display(self.db).fmt(f)?;
}
Ok(())
}
}
impl<'db> UnionType<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> {
DisplayUnionType { db, ty: self }
@@ -375,8 +472,14 @@ impl Display for DisplayStringLiteralType<'_> {
#[cfg(test)]
mod tests {
use ruff_python_ast::name::Name;
use crate::db::tests::setup_db;
use crate::types::{SliceLiteralType, StringLiteralType, Type};
use crate::types::{
KnownClass, Parameter, ParameterKind, Parameters, Signature, SliceLiteralType,
StringLiteralType, Type,
};
use crate::Db;
#[test]
fn test_slice_literal_display() {
@@ -443,4 +546,226 @@ mod tests {
r#"Literal["\""]"#
);
}
fn display_signature<'db>(
db: &dyn Db,
parameters: impl IntoIterator<Item = Parameter<'db>>,
return_ty: Option<Type<'db>>,
) -> String {
Signature::new(Parameters::new(parameters), return_ty)
.display(db)
.to_string()
}
#[test]
fn signature_display() {
let db = setup_db();
// Empty parameters with no return type.
assert_eq!(display_signature(&db, [], None), "() -> Unknown");
// Empty parameters with a return type.
assert_eq!(
display_signature(&db, [], Some(Type::none(&db))),
"() -> None"
);
// Single parameter type (no name) with a return type.
assert_eq!(
display_signature(
&db,
[Parameter::new(
None,
Some(Type::none(&db)),
ParameterKind::PositionalOrKeyword { default_ty: None }
)],
Some(Type::none(&db))
),
"(None) -> None"
);
// Two parameters where one has annotation and the other doesn't.
assert_eq!(
display_signature(
&db,
[
Parameter::new(
Some(Name::new_static("x")),
None,
ParameterKind::PositionalOrKeyword {
default_ty: Some(KnownClass::Int.to_instance(&db))
}
),
Parameter::new(
Some(Name::new_static("y")),
Some(KnownClass::Str.to_instance(&db)),
ParameterKind::PositionalOrKeyword {
default_ty: Some(KnownClass::Str.to_instance(&db))
}
)
],
Some(Type::none(&db))
),
"(x=int, y: str = str) -> None"
);
// All positional only parameters.
assert_eq!(
display_signature(
&db,
[
Parameter::new(
Some(Name::new_static("x")),
None,
ParameterKind::PositionalOnly { default_ty: None }
),
Parameter::new(
Some(Name::new_static("y")),
None,
ParameterKind::PositionalOnly { default_ty: None }
)
],
Some(Type::none(&db))
),
"(x, y, /) -> None"
);
// Positional-only parameters mixed with non-positional-only parameters.
assert_eq!(
display_signature(
&db,
[
Parameter::new(
Some(Name::new_static("x")),
None,
ParameterKind::PositionalOnly { default_ty: None }
),
Parameter::new(
Some(Name::new_static("y")),
None,
ParameterKind::PositionalOrKeyword { default_ty: None }
)
],
Some(Type::none(&db))
),
"(x, /, y) -> None"
);
// All keyword-only parameters.
assert_eq!(
display_signature(
&db,
[
Parameter::new(
Some(Name::new_static("x")),
None,
ParameterKind::KeywordOnly { default_ty: None }
),
Parameter::new(
Some(Name::new_static("y")),
None,
ParameterKind::KeywordOnly { default_ty: None }
)
],
Some(Type::none(&db))
),
"(*, x, y) -> None"
);
// Keyword-only parameters mixed with non-keyword-only parameters.
assert_eq!(
display_signature(
&db,
[
Parameter::new(
Some(Name::new_static("x")),
None,
ParameterKind::PositionalOrKeyword { default_ty: None }
),
Parameter::new(
Some(Name::new_static("y")),
None,
ParameterKind::KeywordOnly { default_ty: None }
)
],
Some(Type::none(&db))
),
"(x, *, y) -> None"
);
// A mix of all parameter kinds.
assert_eq!(
display_signature(
&db,
[
Parameter::new(
Some(Name::new_static("a")),
None,
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("b")),
Some(KnownClass::Int.to_instance(&db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("c")),
None,
ParameterKind::PositionalOnly {
default_ty: Some(Type::IntLiteral(1)),
},
),
Parameter::new(
Some(Name::new_static("d")),
Some(KnownClass::Int.to_instance(&db)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::IntLiteral(2)),
},
),
Parameter::new(
Some(Name::new_static("e")),
None,
ParameterKind::PositionalOrKeyword {
default_ty: Some(Type::IntLiteral(3)),
},
),
Parameter::new(
Some(Name::new_static("f")),
Some(KnownClass::Int.to_instance(&db)),
ParameterKind::PositionalOrKeyword {
default_ty: Some(Type::IntLiteral(4)),
},
),
Parameter::new(
Some(Name::new_static("args")),
Some(Type::object(&db)),
ParameterKind::Variadic,
),
Parameter::new(
Some(Name::new_static("g")),
None,
ParameterKind::KeywordOnly {
default_ty: Some(Type::IntLiteral(5)),
},
),
Parameter::new(
Some(Name::new_static("h")),
Some(KnownClass::Int.to_instance(&db)),
ParameterKind::KeywordOnly {
default_ty: Some(Type::IntLiteral(6)),
},
),
Parameter::new(
Some(Name::new_static("kwargs")),
Some(KnownClass::Str.to_instance(&db)),
ParameterKind::KeywordVariadic,
),
],
Some(KnownClass::Bytes.to_instance(&db))
),
"(a, b: int, c=Literal[1], d: int = Literal[2], \
/, e=Literal[3], f: int = Literal[4], *args: object, \
*, g=Literal[5], h: int = Literal[6], **kwargs: str) -> bytes"
);
}
}

View File

@@ -43,7 +43,7 @@ use crate::module_resolver::{file_to_module, resolve_module};
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId};
use crate::semantic_index::definition::{
AssignmentDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey,
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind,
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind,
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::semantic_index;
@@ -56,24 +56,26 @@ use crate::symbol::{
};
use crate::types::call::{Argument, CallArguments, UnionCallError};
use crate::types::diagnostic::{
report_invalid_arguments_to_annotated, report_invalid_assignment,
report_invalid_attribute_assignment, report_unresolved_module, TypeCheckDiagnostics,
CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS,
CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE,
INCONSISTENT_MRO, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_CONTEXT_MANAGER,
INVALID_DECLARATION, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment, report_unresolved_module,
TypeCheckDiagnostics, CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD,
CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO,
DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS,
POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
};
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType,
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
MetaclassCandidate, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness,
TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers,
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
MetaclassCandidate, Parameter, 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::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
@@ -117,7 +119,14 @@ fn infer_definition_types_cycle_recovery<'db>(
_cycle: &salsa::Cycle,
input: Definition<'db>,
) -> TypeInference<'db> {
tracing::trace!("infer_definition_types_cycle_recovery");
let file = input.file(db);
let _span = tracing::trace_span!(
"infer_definition_types_cycle_recovery",
range = ?input.kind(db).target_range(),
file = %file.path(db)
)
.entered();
TypeInference::cycle_fallback(input.scope(db), todo_type!("cycle recovery"))
}
@@ -315,7 +324,7 @@ impl<'db> TypeInference<'db> {
#[track_caller]
pub(crate) fn expression_type(&self, expression: ScopedExpressionId) -> Type<'db> {
self.try_expression_type(expression).expect(
"expression should belong to this TypeInference region and
"expression should belong to this TypeInference region and \
TypeInferenceBuilder should have inferred a type for it",
)
}
@@ -839,13 +848,8 @@ impl<'db> TypeInferenceBuilder<'db> {
DefinitionKind::Parameter(parameter_with_default) => {
self.infer_parameter_definition(parameter_with_default, definition);
}
DefinitionKind::WithItem(with_item) => {
self.infer_with_item_definition(
with_item.target(),
with_item.node(),
with_item.is_async(),
definition,
);
DefinitionKind::WithItem(with_item_definition) => {
self.infer_with_item_definition(with_item_definition, definition);
}
DefinitionKind::MatchPattern(match_pattern) => {
self.infer_match_pattern_definition(
@@ -937,7 +941,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let declarations = use_def.declarations_at_binding(binding);
let mut bound_ty = ty;
let declared_ty = symbol_from_declarations(self.db(), declarations)
.map(|SymbolAndQualifiers(s, _)| s.ignore_possibly_unbound().unwrap_or(Type::unknown()))
.map(|SymbolAndQualifiers { symbol, .. }| {
symbol.ignore_possibly_unbound().unwrap_or(Type::unknown())
})
.unwrap_or_else(|(ty, conflicting)| {
// TODO point out the conflicting declarations in the diagnostic?
let symbol_table = self.index.symbol_table(binding.file_scope(self.db()));
@@ -1362,7 +1368,9 @@ impl<'db> TypeInferenceBuilder<'db> {
///
/// The annotated type is implicitly wrapped in a homogeneous tuple.
///
/// See `infer_parameter_definition` doc comment for some relevant observations about scopes.
/// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes.
///
/// [`infer_parameter_definition`]: Self::infer_parameter_definition
fn infer_variadic_positional_parameter_definition(
&mut self,
parameter: &ast::Parameter,
@@ -1391,7 +1399,9 @@ impl<'db> TypeInferenceBuilder<'db> {
///
/// The annotated type is implicitly wrapped in a string-keyed dictionary.
///
/// See `infer_parameter_definition` doc comment for some relevant observations about scopes.
/// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes.
///
/// [`infer_parameter_definition`]: Self::infer_parameter_definition
fn infer_variadic_keyword_parameter_definition(
&mut self,
parameter: &ast::Parameter,
@@ -1595,18 +1605,17 @@ impl<'db> TypeInferenceBuilder<'db> {
} = with_statement;
for item in items {
let target = item.optional_vars.as_deref();
if let Some(ast::Expr::Name(name)) = target {
self.infer_definition(name);
if let Some(target) = target {
self.infer_target(target, &item.context_expr, |db, ctx_manager_ty| {
// TODO: `infer_with_statement_definition` reports a diagnostic if `ctx_manager_ty` isn't a context manager
// but only if the target is a name. We should report a diagnostic here if the target isn't a name:
// `with not_context_manager as a.x: ...
ctx_manager_ty.enter(db)
});
} else {
// TODO infer definitions in unpacking assignment
// Call into the context expression inference to validate that it evaluates
// to a valid context manager.
let context_expression_ty = if target.is_some() {
self.infer_standalone_expression(&item.context_expr)
} else {
self.infer_expression(&item.context_expr)
};
let context_expression_ty = self.infer_expression(&item.context_expr);
self.infer_context_expression(&item.context_expr, context_expression_ty, *is_async);
self.infer_optional_expression(target);
}
@@ -1617,24 +1626,36 @@ impl<'db> TypeInferenceBuilder<'db> {
fn infer_with_item_definition(
&mut self,
target: &ast::ExprName,
with_item: &ast::WithItem,
is_async: bool,
with_item: &WithItemDefinitionKind<'db>,
definition: Definition<'db>,
) {
self.infer_standalone_expression(&with_item.context_expr);
let context_expr = with_item.context_expr();
let name = with_item.name();
let target_ty = self.infer_context_expression(
&with_item.context_expr,
self.expression_type(&with_item.context_expr),
is_async,
);
let context_expr_ty = self.infer_standalone_expression(context_expr);
self.types.expressions.insert(
target.scoped_expression_id(self.db(), self.scope()),
target_ty,
);
self.add_binding(target.into(), definition, target_ty);
let target_ty = if with_item.is_async() {
todo_type!("async `with` statement")
} else {
match with_item.target() {
TargetKind::Sequence(unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
if with_item.is_first() {
self.context.extend(unpacked);
}
unpacked.expression_type(name_ast_id)
}
TargetKind::Name => self.infer_context_expression(
context_expr,
context_expr_ty,
with_item.is_async(),
),
}
};
self.store_expression_type(name, target_ty);
self.add_binding(name.into(), definition, target_ty);
}
/// Infers the type of a context expression (`with expr`) and returns the target's type
@@ -1654,120 +1675,12 @@ impl<'db> TypeInferenceBuilder<'db> {
return todo_type!("async `with` statement");
}
let context_manager_ty = context_expression_ty.to_meta_type(self.db());
let enter = context_manager_ty.member(self.db(), "__enter__");
let exit = context_manager_ty.member(self.db(), "__exit__");
// TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`).
match (enter, exit) {
(Symbol::Unbound, Symbol::Unbound) => {
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression,
format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`",
context_expression_ty.display(self.db())
),
);
Type::unknown()
}
(Symbol::Unbound, _) => {
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression,
format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`",
context_expression_ty.display(self.db())
),
);
Type::unknown()
}
(Symbol::Type(enter_ty, enter_boundness), exit) => {
if enter_boundness == Boundness::PossiblyUnbound {
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression,
format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound",
context_expression = context_expression_ty.display(self.db()),
),
);
}
let target_ty = enter_ty
.try_call(self.db(), &CallArguments::positional([context_expression_ty]))
.map(|outcome| outcome.return_type(self.db()))
.unwrap_or_else(|err| {
// TODO: Use more specific error messages for the different error cases.
// E.g. hint toward the union variant that doesn't correctly implement enter,
// distinguish between a not callable `__enter__` attribute and a wrong signature.
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression,
format_args!("
Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__enter__`",
context_expression = context_expression_ty.display(self.db()),
),
);
err.fallback_return_type(self.db())
});
match exit {
Symbol::Unbound => {
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression,
format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`",
context_expression_ty.display(self.db())
),
);
}
Symbol::Type(exit_ty, exit_boundness) => {
// TODO: Use the `exit_ty` to determine if any raised exception is suppressed.
if exit_boundness == Boundness::PossiblyUnbound {
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression,
format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound",
context_expression = context_expression_ty.display(self.db()),
),
);
}
if exit_ty
.try_call(
self.db(),
&CallArguments::positional([
context_manager_ty,
Type::none(self.db()),
Type::none(self.db()),
Type::none(self.db()),
]),
)
.is_err()
{
// TODO: Use more specific error messages for the different error cases.
// E.g. hint toward the union variant that doesn't correctly implement enter,
// distinguish between a not callable `__exit__` attribute and a wrong signature.
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression,
format_args!(
"Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__exit__`",
context_expression = context_expression_ty.display(self.db()),
),
);
}
}
}
target_ty
}
}
context_expression_ty
.try_enter(self.db())
.unwrap_or_else(|err| {
err.report_diagnostic(&self.context, context_expression.into());
err.fallback_enter_type(self.db())
})
}
fn infer_except_handler_definition(
@@ -2353,6 +2266,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Symbol::Type(class_member, boundness) = instance
.class()
.class_member(self.db(), op.in_place_dunder())
.symbol
{
let call = class_member.try_call(
self.db(),
@@ -2768,7 +2682,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} = alias;
// First try loading the requested attribute from the module.
if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), name) {
if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), &name.id).symbol {
if boundness == Boundness::PossiblyUnbound {
// TODO: Consider loading _both_ the attribute and any submodule and unioning them
// together if the attribute exists but is possibly-unbound.
@@ -3380,18 +3294,83 @@ impl<'db> TypeInferenceBuilder<'db> {
body: _,
} = lambda_expression;
if let Some(parameters) = parameters {
for default in parameters
.iter_non_variadic_params()
.filter_map(|param| param.default.as_deref())
{
self.infer_expression(default);
}
let parameters = if let Some(parameters) = parameters {
let positional_only = parameters
.posonlyargs
.iter()
.map(|parameter| {
Parameter::new(
Some(parameter.name().id.clone()),
None,
ParameterKind::PositionalOnly {
default_ty: parameter
.default()
.map(|default| self.infer_expression(default)),
},
)
})
.collect::<Vec<_>>();
let positional_or_keyword = parameters
.args
.iter()
.map(|parameter| {
Parameter::new(
Some(parameter.name().id.clone()),
None,
ParameterKind::PositionalOrKeyword {
default_ty: parameter
.default()
.map(|default| self.infer_expression(default)),
},
)
})
.collect::<Vec<_>>();
let variadic = parameters.vararg.as_ref().map(|parameter| {
Parameter::new(
Some(parameter.name.id.clone()),
None,
ParameterKind::Variadic,
)
});
let keyword_only = parameters
.kwonlyargs
.iter()
.map(|parameter| {
Parameter::new(
Some(parameter.name().id.clone()),
None,
ParameterKind::KeywordOnly {
default_ty: parameter
.default()
.map(|default| self.infer_expression(default)),
},
)
})
.collect::<Vec<_>>();
let keyword_variadic = parameters.kwarg.as_ref().map(|parameter| {
Parameter::new(
Some(parameter.name.id.clone()),
None,
ParameterKind::KeywordVariadic,
)
});
self.infer_parameters(parameters);
}
Parameters::new(
positional_only
.into_iter()
.chain(positional_or_keyword)
.chain(variadic)
.chain(keyword_only)
.chain(keyword_variadic),
)
} else {
Parameters::empty()
};
todo_type!("typing.Callable type")
Type::Callable(CallableType::General(GeneralCallableType::new(
self.db(),
Signature::new(parameters, Some(todo_type!("lambda return type"))),
)))
}
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
@@ -3647,7 +3626,7 @@ impl<'db> TypeInferenceBuilder<'db> {
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
};
let symbol = local_scope_symbol.or_fall_back_to(db, || {
let symbol = SymbolAndQualifiers::from(local_scope_symbol).or_fall_back_to(db, || {
let has_bindings_in_this_scope = match symbol_table.symbol_by_name(symbol_name) {
Some(symbol) => symbol.is_bound(),
None => {
@@ -3669,7 +3648,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// function-like scope, it is considered a local variable; it never references another
// scope. (At runtime, it would use the `LOAD_FAST` opcode.)
if has_bindings_in_this_scope && scope.is_function_like(db) {
return Symbol::Unbound;
return Symbol::Unbound.into();
}
let current_file = self.file();
@@ -3699,7 +3678,7 @@ impl<'db> TypeInferenceBuilder<'db> {
symbol_name,
file_scope_id,
) {
return symbol_from_bindings(db, bindings);
return symbol_from_bindings(db, bindings).into();
}
}
@@ -3718,12 +3697,12 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
Symbol::Unbound
SymbolAndQualifiers::from(Symbol::Unbound)
// No nonlocal binding? Check the module's explicit globals.
// Avoid infinite recursion if `self.scope` already is the module's global scope.
.or_fall_back_to(db, || {
if file_scope_id.is_global() {
return Symbol::Unbound;
return Symbol::Unbound.into();
}
if !self.is_deferred() {
@@ -3732,7 +3711,7 @@ impl<'db> TypeInferenceBuilder<'db> {
symbol_name,
file_scope_id,
) {
return symbol_from_bindings(db, bindings);
return symbol_from_bindings(db, bindings).into();
}
}
@@ -3746,7 +3725,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// (without infinite recursion if we're already in builtins.)
.or_fall_back_to(db, || {
if Some(self.scope()) == builtins_module_scope(db) {
Symbol::Unbound
Symbol::Unbound.into()
} else {
builtins_symbol(db, symbol_name)
}
@@ -3764,21 +3743,23 @@ impl<'db> TypeInferenceBuilder<'db> {
);
typing_extensions_symbol(db, symbol_name)
} else {
Symbol::Unbound
Symbol::Unbound.into()
}
})
});
symbol.unwrap_with_diagnostic(|lookup_error| match lookup_error {
LookupError::Unbound => {
report_unresolved_reference(&self.context, name_node);
Type::unknown()
}
LookupError::PossiblyUnbound(type_when_bound) => {
report_possibly_unresolved_reference(&self.context, name_node);
type_when_bound
}
})
symbol
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
LookupError::Unbound(qualifiers) => {
report_unresolved_reference(&self.context, name_node);
TypeAndQualifiers::new(Type::unknown(), qualifiers)
}
LookupError::PossiblyUnbound(type_when_bound) => {
report_possibly_unresolved_reference(&self.context, name_node);
type_when_bound
}
})
.inner_type()
}
fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> {
@@ -3804,15 +3785,15 @@ impl<'db> TypeInferenceBuilder<'db> {
value_type
.member(db, &attr.id)
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
LookupError::Unbound => {
LookupError::Unbound(_) => {
let bound_on_instance = match value_type {
Type::ClassLiteral(class) => {
!class.class().instance_member(db, attr).0.is_unbound()
!class.class().instance_member(db, attr).symbol.is_unbound()
}
Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => {
match subclass_of.subclass_of() {
ClassBase::Class(class) => {
!class.instance_member(db, attr).0.is_unbound()
!class.instance_member(db, attr).symbol.is_unbound()
}
ClassBase::Dynamic(_) => unreachable!(
"Attribute lookup on a dynamic `SubclassOf` type should always return a bound symbol"
@@ -3844,7 +3825,7 @@ impl<'db> TypeInferenceBuilder<'db> {
);
}
Type::unknown()
Type::unknown().into()
}
LookupError::PossiblyUnbound(type_when_bound) => {
self.context.report_lint(
@@ -3858,7 +3839,7 @@ impl<'db> TypeInferenceBuilder<'db> {
);
type_when_bound
}
})
}).inner_type()
}
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
@@ -3875,8 +3856,8 @@ impl<'db> TypeInferenceBuilder<'db> {
let value_ty = self.infer_expression(value);
let symbol = match value_ty {
Type::Instance(instance) => {
let instance_member = instance.class().instance_member(self.db(), attr);
Type::Instance(_) => {
let instance_member = value_ty.member(self.db(), &attr.id);
if instance_member.is_class_var() {
self.context.report_lint(
&INVALID_ATTRIBUTE_ACCESS,
@@ -3888,10 +3869,10 @@ impl<'db> TypeInferenceBuilder<'db> {
);
}
instance_member.0
instance_member.symbol
}
Type::ClassLiteral(_) | Type::SubclassOf(_) => {
let class_member = value_ty.member(self.db(), attr);
let class_member = value_ty.member(self.db(), &attr.id).symbol;
if class_member.is_unbound() {
let class = match value_ty {
@@ -3905,10 +3886,10 @@ impl<'db> TypeInferenceBuilder<'db> {
_ => None,
};
if let Some(class) = class {
let instance_member = class.instance_member(self.db(), attr);
let instance_member = class.instance_member(self.db(), attr).symbol;
// Attribute is declared or bound on instance. Forbid access from the class object
if !instance_member.0.is_unbound() {
if !instance_member.is_unbound() {
self.context.report_lint(
&INVALID_ATTRIBUTE_ACCESS,
attribute,
@@ -3922,7 +3903,7 @@ impl<'db> TypeInferenceBuilder<'db> {
class_member
}
_ => value_ty.member(self.db(), attr),
_ => value_ty.member(self.db(), &attr.id).symbol,
};
// TODO: The unbound-case might also yield a diagnostic, but we can not activate
@@ -4075,6 +4056,8 @@ impl<'db> TypeInferenceBuilder<'db> {
| (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown),
(todo @ Type::Dynamic(DynamicType::Todo(_)), _, _)
| (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo),
(todo @ Type::Dynamic(DynamicType::TodoProtocol), _, _)
| (_, todo @ Type::Dynamic(DynamicType::TodoProtocol), _) => Some(todo),
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some(
@@ -4244,11 +4227,11 @@ impl<'db> TypeInferenceBuilder<'db> {
let right_class = right_ty.to_meta_type(self.db());
if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) {
let reflected_dunder = op.reflected_dunder();
let rhs_reflected = right_class.member(self.db(), reflected_dunder);
let rhs_reflected = right_class.member(self.db(), reflected_dunder).symbol;
// TODO: if `rhs_reflected` is possibly unbound, we should union the two possible
// CallOutcomes together
if !rhs_reflected.is_unbound()
&& rhs_reflected != left_class.member(self.db(), reflected_dunder)
&& rhs_reflected != left_class.member(self.db(), reflected_dunder).symbol
{
return right_ty
.try_call_dunder(
@@ -4980,7 +4963,7 @@ impl<'db> TypeInferenceBuilder<'db> {
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
let db = self.db();
let contains_dunder = right.class().class_member(db, "__contains__");
let contains_dunder = right.class().class_member(db, "__contains__").symbol;
let compare_result_opt = match contains_dunder {
Symbol::Type(contains_dunder, Boundness::Bound) => {
// If `__contains__` is available, it is used directly for the membership test.
@@ -5248,6 +5231,9 @@ impl<'db> TypeInferenceBuilder<'db> {
value_ty,
Type::IntLiteral(i64::from(bool)),
),
(Type::KnownInstance(KnownInstanceType::Protocol), _) => {
Type::Dynamic(DynamicType::TodoProtocol)
}
(value_ty, slice_ty) => {
// If the class defines `__getitem__`, return its return type.
//
@@ -5299,7 +5285,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// method in these `sys.version_info` branches.
if value_ty.is_subtype_of(self.db(), KnownClass::Type.to_instance(self.db())) {
let dunder_class_getitem_method =
value_ty.member(self.db(), "__class_getitem__");
value_ty.member(self.db(), "__class_getitem__").symbol;
match dunder_class_getitem_method {
Symbol::Unbound => {}
@@ -5353,7 +5339,15 @@ impl<'db> TypeInferenceBuilder<'db> {
);
}
Type::unknown()
match value_ty {
Type::ClassLiteral(_) => {
// TODO: proper support for generic classes
// For now, just infer `Sequence`, if we see something like `Sequence[str]`. This allows us
// to look up attributes on generic base classes, even if we don't understand generics yet.
value_ty
}
_ => Type::unknown(),
}
}
}
}
@@ -5677,14 +5671,16 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: an Ellipsis literal *on its own* does not have any meaning in annotation
// expressions, but is meaningful in the context of a number of special forms.
ast::Expr::EllipsisLiteral(_literal) => todo_type!(),
ast::Expr::EllipsisLiteral(_literal) => {
todo_type!("ellipsis literal in type expression")
}
// Other literals do not have meaningful values in the annotation expression context.
// However, we will we want to handle these differently when working with special forms,
// since (e.g.) `123` is not valid in an annotation expression but `Literal[123]` is.
ast::Expr::BytesLiteral(_literal) => todo_type!(),
ast::Expr::NumberLiteral(_literal) => todo_type!(),
ast::Expr::BooleanLiteral(_literal) => todo_type!(),
ast::Expr::BytesLiteral(_literal) => todo_type!("bytes literal in type expression"),
ast::Expr::NumberLiteral(_literal) => todo_type!("number literal in type expression"),
ast::Expr::BooleanLiteral(_literal) => todo_type!("boolean literal in type expression"),
ast::Expr::Subscript(subscript) => {
let ast::ExprSubscript {
@@ -6084,8 +6080,61 @@ impl<'db> TypeInferenceBuilder<'db> {
todo_type!("Generic PEP-695 type alias")
}
KnownInstanceType::Callable => {
self.infer_type_expression(arguments_slice);
todo_type!("Callable types")
let ast::Expr::Tuple(ast::ExprTuple {
elts: arguments, ..
}) = arguments_slice
else {
report_invalid_arguments_to_callable(self.db(), &self.context, subscript);
// If it's not a tuple, defer it to inferring the parameter types which could
// return an `Err` if the expression is invalid in that position. In which
// case, we'll fallback to using an unknown list of parameters.
let parameters = self
.infer_callable_parameter_types(arguments_slice)
.unwrap_or_else(|()| Parameters::unknown());
let callable_type =
Type::Callable(CallableType::General(GeneralCallableType::new(
self.db(),
Signature::new(parameters, Some(Type::unknown())),
)));
// `Parameters` is not a `Type` variant, so we're storing the outer callable
// type on the arguments slice instead.
self.store_expression_type(arguments_slice, callable_type);
return callable_type;
};
let [first_argument, second_argument] = arguments.as_slice() else {
report_invalid_arguments_to_callable(self.db(), &self.context, subscript);
self.infer_type_expression(arguments_slice);
return Type::Callable(CallableType::General(GeneralCallableType::unknown(
self.db(),
)));
};
let Ok(parameters) = self.infer_callable_parameter_types(first_argument) else {
self.infer_type_expression(arguments_slice);
return Type::Callable(CallableType::General(GeneralCallableType::unknown(
self.db(),
)));
};
let return_type = self.infer_type_expression(second_argument);
let callable_type =
Type::Callable(CallableType::General(GeneralCallableType::new(
self.db(),
Signature::new(parameters, Some(return_type)),
)));
// `Signature` / `Parameters` are not a `Type` variant, so we're storing the outer
// callable type on the these expressions instead.
self.store_expression_type(arguments_slice, callable_type);
self.store_expression_type(first_argument, callable_type);
callable_type
}
// Type API special forms
@@ -6214,6 +6263,10 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_type_expression(arguments_slice);
todo_type!("`Unpack[]` special form")
}
KnownInstanceType::Protocol => {
self.infer_type_expression(arguments_slice);
Type::Dynamic(DynamicType::TodoProtocol)
}
KnownInstanceType::NoReturn
| KnownInstanceType::Never
| KnownInstanceType::Any
@@ -6320,6 +6373,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: Check that value type is enum otherwise return None
value_ty
.member(self.db(), &attr.id)
.symbol
.ignore_possibly_unbound()
.unwrap_or(Type::unknown())
}
@@ -6336,6 +6390,73 @@ impl<'db> TypeInferenceBuilder<'db> {
}
})
}
/// Infer the first argument to a `typing.Callable` type expression and returns the
/// corresponding [`Parameters`].
///
/// It returns an [`Err`] if the argument is invalid i.e., not a list of types, parameter
/// specification, `typing.Concatenate`, or `...`.
fn infer_callable_parameter_types(
&mut self,
parameters: &ast::Expr,
) -> Result<Parameters<'db>, ()> {
Ok(match parameters {
ast::Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { .. }) => {
Parameters::gradual_form()
}
ast::Expr::List(ast::ExprList { elts: params, .. }) => {
let mut parameter_types = Vec::with_capacity(params.len());
// Whether to infer `Todo` for the parameters
let mut return_todo = false;
for param in params {
let param_type = self.infer_type_expression(param);
// This is similar to what we currently do for inferring tuple type expression.
// We currently infer `Todo` for the parameters to avoid invalid diagnostics
// when trying to check for assignability or any other relation. For example,
// `*tuple[int, str]`, `Unpack[]`, etc. are not yet supported.
return_todo |= param_type.is_todo()
&& matches!(param, ast::Expr::Starred(_) | ast::Expr::Subscript(_));
parameter_types.push(param_type);
}
if return_todo {
// TODO: `Unpack`
Parameters::todo()
} else {
Parameters::new(parameter_types.iter().map(|param_type| {
Parameter::new(
None,
Some(*param_type),
ParameterKind::PositionalOnly { default_ty: None },
)
}))
}
}
ast::Expr::Subscript(_) => {
// TODO: Support `Concatenate[...]`
Parameters::todo()
}
ast::Expr::Name(name) if name.is_invalid() => {
// This is a special case to avoid raising the error suggesting what the first
// argument should be. This only happens when there's already a syntax error like
// `Callable[]`.
return Err(());
}
_ => {
// TODO: Check whether `Expr::Name` is a ParamSpec
self.context.report_lint(
&INVALID_TYPE_FORM,
parameters,
format_args!(
"The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`",
),
);
return Err(());
}
})
}
}
/// The deferred state of a specific expression in an inference region.
@@ -6577,7 +6698,7 @@ mod tests {
assert_eq!(scope.name(db), *expected_scope_name);
}
symbol(db, scope, symbol_name)
symbol(db, scope, symbol_name).symbol
}
#[track_caller]
@@ -6732,7 +6853,7 @@ mod tests {
assert_eq!(var_ty.display(&db).to_string(), var);
let expected_name_ty = format!(r#"Literal["{var}"]"#);
let name_ty = var_ty.member(&db, "__name__").expect_type();
let name_ty = var_ty.member(&db, "__name__").symbol.expect_type();
assert_eq!(name_ty.display(&db).to_string(), expected_name_ty);
let KnownInstanceType::TypeVar(typevar) = var_ty.expect_known_instance() else {
@@ -6793,7 +6914,7 @@ mod tests {
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol(&db, a, "x").expect_type();
let x_ty = global_symbol(&db, a, "x").symbol.expect_type();
assert_eq!(x_ty.display(&db).to_string(), "int");
@@ -6802,7 +6923,7 @@ mod tests {
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type();
assert_eq!(x_ty_2.display(&db).to_string(), "bool");
@@ -6819,7 +6940,7 @@ mod tests {
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol(&db, a, "x").expect_type();
let x_ty = global_symbol(&db, a, "x").symbol.expect_type();
assert_eq!(x_ty.display(&db).to_string(), "int");
@@ -6829,7 +6950,7 @@ mod tests {
db.clear_salsa_events();
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type();
assert_eq!(x_ty_2.display(&db).to_string(), "int");
@@ -6855,7 +6976,7 @@ mod tests {
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol(&db, a, "x").expect_type();
let x_ty = global_symbol(&db, a, "x").symbol.expect_type();
assert_eq!(x_ty.display(&db).to_string(), "int");
@@ -6865,7 +6986,7 @@ mod tests {
db.clear_salsa_events();
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type();
assert_eq!(x_ty_2.display(&db).to_string(), "int");
@@ -6912,7 +7033,7 @@ mod tests {
)?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
@@ -6927,7 +7048,7 @@ mod tests {
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
@@ -6946,7 +7067,7 @@ mod tests {
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
@@ -6997,7 +7118,7 @@ mod tests {
)?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
@@ -7014,7 +7135,7 @@ mod tests {
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
@@ -7035,7 +7156,7 @@ mod tests {
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};

View File

@@ -100,13 +100,16 @@ impl Ty {
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
Ty::LiteralString => Type::LiteralString,
Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()),
Ty::BuiltinInstance(s) => builtins_symbol(db, s).expect_type().to_instance(db),
Ty::BuiltinInstance(s) => builtins_symbol(db, s).symbol.expect_type().to_instance(db),
Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s)
.symbol
.expect_type()
.to_instance(db),
Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s).expect_type(),
Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s)
.symbol
.expect_type(),
Ty::TypingLiteral => Type::KnownInstance(KnownInstanceType::Literal),
Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).expect_type(),
Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).symbol.expect_type(),
Ty::KnownClassInstance(known_class) => known_class.to_instance(db),
Ty::Union(tys) => {
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
@@ -129,6 +132,7 @@ impl Ty {
Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from(
db,
builtins_symbol(db, s)
.symbol
.expect_type()
.expect_class_literal()
.class,
@@ -136,16 +140,17 @@ impl Ty {
Ty::SubclassOfAbcClass(s) => SubclassOfType::from(
db,
known_module_symbol(db, KnownModule::Abc, s)
.symbol
.expect_type()
.expect_class_literal()
.class,
),
Ty::AlwaysTruthy => Type::AlwaysTruthy,
Ty::AlwaysFalsy => Type::AlwaysFalsy,
Ty::BuiltinsFunction(name) => builtins_symbol(db, name).expect_type(),
Ty::BuiltinsFunction(name) => builtins_symbol(db, name).symbol.expect_type(),
Ty::BuiltinsBoundMethod { class, method } => {
let builtins_class = builtins_symbol(db, class).expect_type();
let function = builtins_class.static_member(db, method).expect_type();
let builtins_class = builtins_symbol(db, class).symbol.expect_type();
let function = builtins_class.member(db, method).symbol.expect_type();
create_bound_method(db, function, builtins_class)
}

View File

@@ -1,11 +1,11 @@
use super::{definition_expression_type, Type};
use super::{definition_expression_type, DynamicType, Type};
use crate::Db;
use crate::{semantic_index::definition::Definition, types::todo_type};
use ruff_python_ast::{self as ast, name::Name};
/// A typed callable signature.
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct Signature<'db> {
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub struct Signature<'db> {
/// Parameters, in source order.
///
/// The ordering of parameters in a valid signature must be: first positional-only parameters,
@@ -67,29 +67,113 @@ impl<'db> Signature<'db> {
}
}
// TODO: use SmallVec here once invariance bug is fixed
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub(crate) struct Parameters<'db> {
// TODO: use SmallVec here once invariance bug is fixed
value: Vec<Parameter<'db>>,
/// Whether this parameter list represents a gradual form using `...` as the only parameter.
///
/// If this is `true`, the `value` will still contain the variadic and keyword-variadic
/// parameters. This flag is used to distinguish between an explicit `...` in the callable type
/// as in `Callable[..., int]` and the variadic arguments in `lambda` expression as in
/// `lambda *args, **kwargs: None`.
///
/// The display implementation utilizes this flag to use `...` instead of displaying the
/// individual variadic and keyword-variadic parameters.
///
/// Note: This flag is also used to indicate invalid forms of `Callable` annotations.
is_gradual: bool,
}
impl<'db> Parameters<'db> {
pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self {
Self(parameters.into_iter().collect())
Self {
value: parameters.into_iter().collect(),
is_gradual: false,
}
}
/// Create an empty parameter list.
pub(crate) fn empty() -> Self {
Self {
value: Vec::new(),
is_gradual: false,
}
}
pub(crate) fn as_slice(&self) -> &[Parameter<'db>] {
self.value.as_slice()
}
pub(crate) const fn is_gradual(&self) -> bool {
self.is_gradual
}
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
fn todo() -> Self {
Self(vec![
Parameter {
name: Some(Name::new_static("args")),
annotated_ty: Some(todo_type!("todo signature *args")),
kind: ParameterKind::Variadic,
},
Parameter {
name: Some(Name::new_static("kwargs")),
annotated_ty: Some(todo_type!("todo signature **kwargs")),
kind: ParameterKind::KeywordVariadic,
},
])
pub(crate) fn todo() -> Self {
Self {
value: vec![
Parameter {
name: Some(Name::new_static("args")),
annotated_ty: Some(todo_type!("todo signature *args")),
kind: ParameterKind::Variadic,
},
Parameter {
name: Some(Name::new_static("kwargs")),
annotated_ty: Some(todo_type!("todo signature **kwargs")),
kind: ParameterKind::KeywordVariadic,
},
],
is_gradual: false,
}
}
/// Return parameters that represents a gradual form using `...` as the only parameter.
///
/// Internally, this is represented as `(*Any, **Any)` that accepts parameters of type [`Any`].
///
/// [`Any`]: crate::types::DynamicType::Any
pub(crate) fn gradual_form() -> Self {
Self {
value: vec![
Parameter {
name: None,
annotated_ty: Some(Type::Dynamic(DynamicType::Any)),
kind: ParameterKind::Variadic,
},
Parameter {
name: None,
annotated_ty: Some(Type::Dynamic(DynamicType::Any)),
kind: ParameterKind::KeywordVariadic,
},
],
is_gradual: true,
}
}
/// Return parameters that represents an unknown list of parameters.
///
/// Internally, this is represented as `(*Unknown, **Unknown)` that accepts parameters of type
/// [`Unknown`].
///
/// [`Unknown`]: crate::types::DynamicType::Unknown
pub(crate) fn unknown() -> Self {
Self {
value: vec![
Parameter {
name: None,
annotated_ty: Some(Type::Dynamic(DynamicType::Unknown)),
kind: ParameterKind::Variadic,
},
Parameter {
name: None,
annotated_ty: Some(Type::Dynamic(DynamicType::Unknown)),
kind: ParameterKind::KeywordVariadic,
},
],
is_gradual: true,
}
}
fn from_parameters(
@@ -146,22 +230,21 @@ impl<'db> Parameters<'db> {
let keywords = kwarg.as_ref().map(|arg| {
Parameter::from_node_and_kind(db, definition, arg, ParameterKind::KeywordVariadic)
});
Self(
Self::new(
positional_only
.chain(positional_or_keyword)
.chain(variadic)
.chain(keyword_only)
.chain(keywords)
.collect(),
.chain(keywords),
)
}
pub(crate) fn len(&self) -> usize {
self.0.len()
self.value.len()
}
pub(crate) fn iter(&self) -> std::slice::Iter<Parameter<'db>> {
self.0.iter()
self.value.iter()
}
/// Iterate initial positional parameters, not including variadic parameter, if any.
@@ -175,7 +258,7 @@ impl<'db> Parameters<'db> {
/// Return parameter at given index, or `None` if index is out-of-range.
pub(crate) fn get(&self, index: usize) -> Option<&Parameter<'db>> {
self.0.get(index)
self.value.get(index)
}
/// Return positional parameter at given index, or `None` if `index` is out of range.
@@ -218,7 +301,7 @@ impl<'db, 'a> IntoIterator for &'a Parameters<'db> {
type IntoIter = std::slice::Iter<'a, Parameter<'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
self.value.iter()
}
}
@@ -226,11 +309,11 @@ impl<'db> std::ops::Index<usize> for Parameters<'db> {
type Output = Parameter<'db>;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
&self.value[index]
}
}
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub(crate) struct Parameter<'db> {
/// Parameter name.
///
@@ -272,6 +355,14 @@ impl<'db> Parameter<'db> {
}
}
pub(crate) fn is_keyword_only(&self) -> bool {
matches!(self.kind, ParameterKind::KeywordOnly { .. })
}
pub(crate) fn is_positional_only(&self) -> bool {
matches!(self.kind, ParameterKind::PositionalOnly { .. })
}
pub(crate) fn is_variadic(&self) -> bool {
matches!(self.kind, ParameterKind::Variadic)
}
@@ -328,7 +419,7 @@ impl<'db> Parameter<'db> {
}
}
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub(crate) enum ParameterKind<'db> {
/// Positional-only parameter, e.g. `def f(x, /): ...`
PositionalOnly { default_ty: Option<Type<'db>> },
@@ -354,13 +445,14 @@ mod tests {
fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> {
let module = ruff_db::files::system_path_to_file(db, file).unwrap();
global_symbol(db, module, "f")
.symbol
.expect_type()
.expect_function_literal()
}
#[track_caller]
fn assert_params<'db>(signature: &Signature<'db>, expected: &[Parameter<'db>]) {
assert_eq!(signature.parameters.0.as_slice(), expected);
assert_eq!(signature.parameters.value.as_slice(), expected);
}
#[test]
@@ -489,7 +581,7 @@ mod tests {
name: Some(name),
annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
}] = &sig.parameters.value[..]
else {
panic!("expected one positional-or-keyword parameter");
};
@@ -523,7 +615,7 @@ mod tests {
name: Some(name),
annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
}] = &sig.parameters.value[..]
else {
panic!("expected one positional-or-keyword parameter");
};
@@ -561,7 +653,7 @@ mod tests {
name: Some(b_name),
annotated_ty: b_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
}] = &sig.parameters.value[..]
else {
panic!("expected two positional-or-keyword parameters");
};
@@ -604,7 +696,7 @@ mod tests {
name: Some(b_name),
annotated_ty: b_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
}] = &sig.parameters.value[..]
else {
panic!("expected two positional-or-keyword parameters");
};

View File

@@ -24,7 +24,7 @@ enum SlotsKind {
impl SlotsKind {
fn from(db: &dyn Db, base: Class) -> Self {
let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__") else {
let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__").symbol else {
return Self::NotSpecified;
};

View File

@@ -1,4 +1,6 @@
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Symbol, Type};
use crate::symbol::SymbolAndQualifiers;
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Type};
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
@@ -64,8 +66,12 @@ impl<'db> SubclassOfType<'db> {
!self.is_dynamic()
}
pub(crate) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
Type::from(self.subclass_of).static_member(db, name)
pub(crate) fn find_name_in_mro(
self,
db: &'db dyn Db,
name: &str,
) -> Option<SymbolAndQualifiers<'db>> {
Type::from(self.subclass_of).find_name_in_mro(db, name)
}
/// Return `true` if `self` is a subtype of `other`.

View File

@@ -77,6 +77,12 @@ pub(super) fn union_elements_ordering<'db>(left: &Type<'db>, right: &Type<'db>)
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
(Type::Callable(CallableType::General(_)), Type::Callable(CallableType::General(_))) => {
Ordering::Equal
}
(Type::Callable(CallableType::General(_)), _) => Ordering::Less,
(_, Type::Callable(CallableType::General(_))) => Ordering::Greater,
(Type::Tuple(left), Type::Tuple(right)) => left.cmp(right),
(Type::Tuple(_), _) => Ordering::Less,
(_, Type::Tuple(_)) => Ordering::Greater,
@@ -184,6 +190,9 @@ pub(super) fn union_elements_ordering<'db>(left: &Type<'db>, right: &Type<'db>)
(KnownInstanceType::OrderedDict, _) => Ordering::Less,
(_, KnownInstanceType::OrderedDict) => Ordering::Greater,
(KnownInstanceType::Protocol, _) => Ordering::Less,
(_, KnownInstanceType::Protocol) => Ordering::Greater,
(KnownInstanceType::NoReturn, _) => Ordering::Less,
(_, KnownInstanceType::NoReturn) => Ordering::Greater,
@@ -285,5 +294,8 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
#[cfg(not(debug_assertions))]
(DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal,
(DynamicType::TodoProtocol, _) => Ordering::Less,
(_, DynamicType::TodoProtocol) => Ordering::Greater,
}
}

View File

@@ -42,26 +42,28 @@ impl<'db> Unpacker<'db> {
"Unpacking target must be a list or tuple expression"
);
let mut value_ty = infer_expression_types(self.db(), value.expression())
let value_ty = infer_expression_types(self.db(), value.expression())
.expression_type(value.scoped_expression_id(self.db(), self.scope));
if value.is_assign()
&& self.context.in_stub()
&& value
.expression()
.node_ref(self.db())
.is_ellipsis_literal_expr()
{
value_ty = Type::unknown();
}
if value.is_iterable() {
// If the value is an iterable, then the type that needs to be unpacked is the iterator
// type.
value_ty = value_ty.try_iterate(self.db()).unwrap_or_else(|err| {
let value_ty = match value {
UnpackValue::Assign(expression) => {
if self.context.in_stub()
&& expression.node_ref(self.db()).is_ellipsis_literal_expr()
{
Type::unknown()
} else {
value_ty
}
}
UnpackValue::Iterable(_) => value_ty.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, value.as_any_node_ref(self.db()));
err.fallback_element_type(self.db())
});
}
}),
UnpackValue::ContextManager(_) => value_ty.try_enter(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, value.as_any_node_ref(self.db()));
err.fallback_enter_type(self.db())
}),
};
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_ty);
}
@@ -121,7 +123,7 @@ impl<'db> Unpacker<'db> {
if let Some(tuple_ty) = ty.into_tuple() {
let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty);
match elts.len().cmp(&tuple_ty_elements.len()) {
let length_mismatch = match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
self.context.report_lint(
&INVALID_ASSIGNMENT,
@@ -132,6 +134,7 @@ impl<'db> Unpacker<'db> {
tuple_ty_elements.len()
),
);
true
}
Ordering::Greater => {
self.context.report_lint(
@@ -143,13 +146,18 @@ impl<'db> Unpacker<'db> {
tuple_ty_elements.len()
),
);
true
}
Ordering::Equal => {}
}
Ordering::Equal => false,
};
for (index, ty) in tuple_ty_elements.iter().enumerate() {
if let Some(element_types) = target_types.get_mut(index) {
element_types.push(*ty);
if length_mismatch {
element_types.push(Type::unknown());
} else {
element_types.push(*ty);
}
}
}
} else {
@@ -243,15 +251,7 @@ impl<'db> Unpacker<'db> {
),
);
let mut element_types = tuple_ty.elements(self.db()).to_vec();
// Subtract 1 to insert the starred expression type at the correct
// index.
element_types.resize(targets.len() - 1, Type::unknown());
// TODO: This should be `list[Unknown]`
element_types.insert(starred_index, todo_type!("starred unpacking"));
Cow::Owned(element_types)
Cow::Owned(vec![Type::unknown(); targets.len()])
}
}

View File

@@ -63,25 +63,19 @@ impl<'db> Unpack<'db> {
pub(crate) enum UnpackValue<'db> {
/// An iterable expression like the one in a `for` loop or a comprehension.
Iterable(Expression<'db>),
/// An context manager expression like the one in a `with` statement.
ContextManager(Expression<'db>),
/// An expression that is being assigned to a target.
Assign(Expression<'db>),
}
impl<'db> UnpackValue<'db> {
/// Returns `true` if the value is an iterable expression.
pub(crate) const fn is_iterable(self) -> bool {
matches!(self, UnpackValue::Iterable(_))
}
/// Returns `true` if the value is being assigned to a target.
pub(crate) const fn is_assign(self) -> bool {
matches!(self, UnpackValue::Assign(_))
}
/// Returns the underlying [`Expression`] that is being unpacked.
pub(crate) const fn expression(self) -> Expression<'db> {
match self {
UnpackValue::Assign(expr) | UnpackValue::Iterable(expr) => expr,
UnpackValue::Assign(expr)
| UnpackValue::Iterable(expr)
| UnpackValue::ContextManager(expr) => expr,
}
}

4
crates/red_knot_vendored/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Do not ignore any of the vendored files. If this pattern is not present,
# we will gitignore the `venv/` stubs in typeshed, as there is a general
# rule to ignore `venv/` directories in the root `.gitignore`.
!/vendor/typeshed/**/*

View File

@@ -0,0 +1,107 @@
import logging
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Iterable, Sequence
from types import SimpleNamespace
logger: logging.Logger
if sys.version_info >= (3, 9):
CORE_VENV_DEPS: tuple[str, ...]
class EnvBuilder:
system_site_packages: bool
clear: bool
symlinks: bool
upgrade: bool
with_pip: bool
prompt: str | None
if sys.version_info >= (3, 13):
def __init__(
self,
system_site_packages: bool = False,
clear: bool = False,
symlinks: bool = False,
upgrade: bool = False,
with_pip: bool = False,
prompt: str | None = None,
upgrade_deps: bool = False,
*,
scm_ignore_files: Iterable[str] = ...,
) -> None: ...
elif sys.version_info >= (3, 9):
def __init__(
self,
system_site_packages: bool = False,
clear: bool = False,
symlinks: bool = False,
upgrade: bool = False,
with_pip: bool = False,
prompt: str | None = None,
upgrade_deps: bool = False,
) -> None: ...
else:
def __init__(
self,
system_site_packages: bool = False,
clear: bool = False,
symlinks: bool = False,
upgrade: bool = False,
with_pip: bool = False,
prompt: str | None = None,
) -> None: ...
def create(self, env_dir: StrOrBytesPath) -> None: ...
def clear_directory(self, path: StrOrBytesPath) -> None: ... # undocumented
def ensure_directories(self, env_dir: StrOrBytesPath) -> SimpleNamespace: ...
def create_configuration(self, context: SimpleNamespace) -> None: ...
def symlink_or_copy(
self, src: StrOrBytesPath, dst: StrOrBytesPath, relative_symlinks_ok: bool = False
) -> None: ... # undocumented
def setup_python(self, context: SimpleNamespace) -> None: ...
def _setup_pip(self, context: SimpleNamespace) -> None: ... # undocumented
def setup_scripts(self, context: SimpleNamespace) -> None: ...
def post_setup(self, context: SimpleNamespace) -> None: ...
def replace_variables(self, text: str, context: SimpleNamespace) -> str: ... # undocumented
def install_scripts(self, context: SimpleNamespace, path: str) -> None: ...
if sys.version_info >= (3, 9):
def upgrade_dependencies(self, context: SimpleNamespace) -> None: ...
if sys.version_info >= (3, 13):
def create_git_ignore_file(self, context: SimpleNamespace) -> None: ...
if sys.version_info >= (3, 13):
def create(
env_dir: StrOrBytesPath,
system_site_packages: bool = False,
clear: bool = False,
symlinks: bool = False,
with_pip: bool = False,
prompt: str | None = None,
upgrade_deps: bool = False,
*,
scm_ignore_files: Iterable[str] = ...,
) -> None: ...
elif sys.version_info >= (3, 9):
def create(
env_dir: StrOrBytesPath,
system_site_packages: bool = False,
clear: bool = False,
symlinks: bool = False,
with_pip: bool = False,
prompt: str | None = None,
upgrade_deps: bool = False,
) -> None: ...
else:
def create(
env_dir: StrOrBytesPath,
system_site_packages: bool = False,
clear: bool = False,
symlinks: bool = False,
with_pip: bool = False,
prompt: str | None = None,
) -> None: ...
def main(args: Sequence[str] | None = None) -> None: ...

View File

@@ -45,3 +45,10 @@ crypt.crypt("test", salt=crypt.METHOD_SHA512)
crypt.mksalt()
crypt.mksalt(crypt.METHOD_SHA256)
crypt.mksalt(crypt.METHOD_SHA512)
# From issue: https://github.com/astral-sh/ruff/issues/16525#issuecomment-2706188584
# Errors
hashlib.new("Md5")
# OK
hashlib.new('Sha256')

View File

@@ -135,11 +135,11 @@ fn detect_insecure_hashlib_calls(
return;
};
// `hashlib.new` accepts both lowercase and uppercase names for hash
// `hashlib.new` accepts mixed lowercase and uppercase names for hash
// functions.
if matches!(
hash_func_name,
"md4" | "md5" | "sha" | "sha1" | "MD4" | "MD5" | "SHA" | "SHA1"
hash_func_name.to_ascii_lowercase().as_str(),
"md4" | "md5" | "sha" | "sha1"
) {
checker.report_diagnostic(Diagnostic::new(
HashlibInsecureHashFunction {

View File

@@ -195,3 +195,13 @@ S324.py:29:14: S324 Probable use of insecure hash functions in `crypt`: `crypt.M
30 |
31 | # OK
|
S324.py:51:13: S324 Probable use of insecure hash functions in `hashlib`: `Md5`
|
49 | # From issue: https://github.com/astral-sh/ruff/issues/16525#issuecomment-2706188584
50 | # Errors
51 | hashlib.new("Md5")
| ^^^^^ S324
52 |
53 | # OK
|

View File

@@ -37,6 +37,8 @@ use crate::rules::pep8_naming::helpers;
///
/// ## Options
/// - `lint.flake8-import-conventions.aliases`
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/
#[derive(ViolationMetadata)]

View File

@@ -44,6 +44,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// A common example of a single uppercase character being used for a class
/// name can be found in Django's `django.db.models.Q` class.
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/
#[derive(ViolationMetadata)]
pub(crate) struct CamelcaseImportedAsConstant {

View File

@@ -29,6 +29,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// from example import MyClassName
/// ```
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/
#[derive(ViolationMetadata)]
pub(crate) struct CamelcaseImportedAsLowercase {

View File

@@ -42,6 +42,10 @@ use crate::rules::pep8_naming::{helpers, settings::IgnoreNames};
/// A common example of a single uppercase character being used for a class
/// name can be found in Django's `django.db.models.Q` class.
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/
#[derive(ViolationMetadata)]
pub(crate) struct ConstantImportedAsNonConstant {

View File

@@ -31,6 +31,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// pass
/// ```
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/
#[derive(ViolationMetadata)]
pub(crate) struct DunderFunctionName;

View File

@@ -28,6 +28,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// class ValidationError(Exception): ...
/// ```
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#exception-names
#[derive(ViolationMetadata)]
pub(crate) struct ErrorSuffixOnExceptionName {

View File

@@ -37,6 +37,10 @@ use crate::checkers::ast::Checker;
/// pass
/// ```
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]

View File

@@ -34,6 +34,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// pass
/// ```
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#class-names
#[derive(ViolationMetadata)]
pub(crate) struct InvalidClassName {

View File

@@ -28,6 +28,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// from example import myclassname
/// ```
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/
#[derive(ViolationMetadata)]
pub(crate) struct LowercaseImportedAsNonLowercase {

View File

@@ -35,6 +35,10 @@ use crate::rules::pep8_naming::helpers;
/// another_variable = "world"
/// ```
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments
#[derive(ViolationMetadata)]
pub(crate) struct MixedCaseVariableInClassScope {

View File

@@ -46,6 +46,10 @@ use crate::rules::pep8_naming::helpers;
/// yet_another_variable = "foo"
/// ```
///
/// ## Options
/// - `lint.pep8-naming.ignore-names`
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#global-variable-names
#[derive(ViolationMetadata)]
pub(crate) struct MixedCaseVariableInGlobalScope {

View File

@@ -2179,6 +2179,13 @@ impl ExprName {
pub fn id(&self) -> &Name {
&self.id
}
/// Returns `true` if this node represents an invalid name i.e., the `ctx` is [`Invalid`].
///
/// [`Invalid`]: ExprContext::Invalid
pub const fn is_invalid(&self) -> bool {
matches!(self.ctx, ExprContext::Invalid)
}
}
impl ExprList {

View File

@@ -1,16 +1,17 @@
# Migrating from `ruff-lsp`
To provide some context, [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) is the LSP implementation for Ruff to power the editor
integrations which is written in Python and is a separate package from Ruff itself. The **native
server** is the LSP implementation which is written in Rust and is available under the `ruff server`
command. This guide is intended to help users migrate from
[`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) to the native server.
[`ruff-lsp`][ruff-lsp] is the [Language Server Protocol] implementation for Ruff to power the editor
integrations. It is written in Python and is a separate package from Ruff itself. The **native
server**, however, is the [Language Server Protocol] implementation which is **written in Rust** and
is available under the `ruff server` command. This guide is intended to help users migrate from
[`ruff-lsp`][ruff-lsp] to the native server.
!!! note
The native server was first introduced in Ruff version `0.3.5`. It was marked as beta in version
`0.4.5` and officially stabilized in version `0.5.3`. It is recommended to use the latest
version of Ruff to ensure the best experience.
The native server was first introduced in Ruff version `0.3.5`. It was marked as [beta in
version `0.4.5`](https://astral.sh/blog/ruff-v0.4.5) and officially [stabilized in version
`0.5.3`](https://github.com/astral-sh/ruff/releases/tag/0.5.3). It is recommended to use the
latest version of Ruff to ensure the best experience.
The migration process involves any or all of the following:
@@ -18,32 +19,36 @@ The migration process involves any or all of the following:
1. [Remove settings](#removed-settings) that are no longer supported
1. Update the `ruff` version
Read on to learn more about the unsupported or new settings, or jump to the [examples](#examples)
that enumerate some of the common settings and how to migrate them.
## Unsupported Settings
The following [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) settings are not supported by `ruff server`:
The following [`ruff-lsp`][ruff-lsp] settings are not supported by the native server:
- `lint.run`: This setting is no longer relevant for the native language server, which runs on every
keystroke by default.
- `lint.args`, `format.args`: These settings have been replaced by more granular settings in `ruff server` like [`lint.select`](settings.md#select), [`format.preview`](settings.md#format_preview),
etc. along with the ability to provide a default configuration file using [`configuration`](settings.md#configuration).
- [`lint.run`](settings.md#lintrun): This setting is no longer relevant for the native language
server, which runs on every keystroke by default.
- [`lint.args`](settings.md#lintargs), [`format.args`](settings.md#formatargs): These settings have
been replaced by more granular settings in the native server like [`lint.select`](settings.md#select),
[`format.preview`](settings.md#format_preview), etc. along with the ability to override any
configuration using the [`configuration`](settings.md#configuration) setting.
The following settings are not accepted by the language server but are still used by the VS Code
extension. Refer to their respective documentation for more information on how it's being used by
the extension:
The following settings are not accepted by the language server but are still used by the [VS Code extension].
Refer to their respective documentation for more information on how each is used by the extension:
- [`path`](settings.md#path)
- [`interpreter`](settings.md#interpreter)
## Removed Settings
Additionally, the following settings are not supported by the native server, they should be removed:
Additionally, the following settings are not supported by the native server and should be removed:
- `ignoreStandardLibrary`
- `showNotifications`
- [`ignoreStandardLibrary`](settings.md#ignorestandardlibrary)
- [`showNotifications`](settings.md#shownotifications)
## New Settings
`ruff server` introduces several new settings that [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) does not have. These are, as follows:
The native server introduces several new settings that [`ruff-lsp`][ruff-lsp] does not have:
- [`configuration`](settings.md#configuration)
- [`configurationPreference`](settings.md#configurationpreference)
@@ -55,44 +60,96 @@ Additionally, the following settings are not supported by the native server, the
- [`lint.ignore`](settings.md#ignore)
- [`lint.preview`](settings.md#lint_preview)
Several of these new settings are replacements for the now-unsupported `format.args` and `lint.args`. For example, if
you've been passing `--select=<RULES>` to `lint.args`, you can migrate to the new server by using `lint.select` with a
value of `["<RULES>"]`.
## Examples
Let's say you have these settings in VS Code:
All of the examples mentioned below are only valid for the [VS Code extension]. For other editors,
please refer to their respective documentation sections in the [settings](settings.md) page.
### Configuration file
If you've been providing a configuration file as shown below:
```json
{
"ruff.lint.args": "--select=E,F --line-length 80 --config ~/.config/custom_ruff_config.toml"
"ruff.lint.args": "--config ~/.config/custom_ruff_config.toml",
"ruff.format.args": "--config ~/.config/custom_ruff_config.toml"
}
```
After enabling the native server, you can migrate your settings like so:
You can migrate to the new server by using the [`configuration`](settings.md#configuration) setting
like below which will apply the configuration to both the linter and the formatter:
```json
{
"ruff.configuration": "~/.config/custom_ruff_config.toml"
}
```
### `lint.args`
If you're providing the linter flags by using `ruff.lint.args` like so:
```json
{
"ruff.lint.args": "--select=E,F --unfixable=F401 --unsafe-fixes"
}
```
You can migrate to the new server by using the [`lint.select`](settings.md#select) and
[`configuration`](settings.md#configuration) setting like so:
```json
{
"ruff.lint.select": ["E", "F"],
"ruff.configuration": {
"unsafe-fixes": true,
"lint": {
"unfixable": ["F401"]
}
}
}
```
The following options can be set directly in the editor settings:
- [`lint.select`](settings.md#select)
- [`lint.extendSelect`](settings.md#extendselect)
- [`lint.ignore`](settings.md#ignore)
- [`lint.preview`](settings.md#lint_preview)
The remaining options can be set using the [`configuration`](settings.md#configuration) setting.
### `format.args`
If you're also providing formatter flags by using `ruff.format.args` like so:
```json
{
"ruff.format.args": "--line-length 80 --config='format.quote-style=double'"
}
```
You can migrate to the new server by using the [`lineLength`](settings.md#linelength) and
[`configuration`](settings.md#configuration) setting like so:
```json
{
"ruff.configuration": "~/.config/custom_ruff_config.toml",
"ruff.lineLength": 80,
"ruff.lint.select": ["E", "F"]
"ruff.configuration": {
"format": {
"quote-style": "double"
}
}
}
```
Similarly, let's say you have these settings in Helix:
The following options can be set directly in the editor settings:
```toml
[language-server.ruff.config.lint]
args = "--select=E,F --line-length 80 --config ~/.config/custom_ruff_config.toml"
```
- [`lineLength`](settings.md#linelength)
- [`format.preview`](settings.md#format_preview)
These can be migrated like so:
The remaining options can be set using the [`configuration`](settings.md#configuration) setting.
```toml
[language-server.ruff.config]
configuration = "~/.config/custom_ruff_config.toml"
lineLength = 80
[language-server.ruff.config.lint]
select = ["E", "F"]
```
[language server protocol]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
[ruff-lsp]: https://github.com/astral-sh/ruff-lsp
[vs code extension]: https://github.com/astral-sh/ruff-vscode

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.2
ruff==0.9.9
ruff==0.9.10
mkdocs==1.6.1
mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff
mkdocs-redirects==1.2.2

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.2
ruff==0.9.9
ruff==0.9.10
mkdocs==1.6.1
mkdocs-material==9.5.38
mkdocs-redirects==1.2.2