Compare commits

...

55 Commits

Author SHA1 Message Date
David Peter
18bb0a36b6 [red-knot] Separate 'infer_expression_type' query 2025-02-04 16:42:49 +01:00
Alex Waygood
5bf0e2e95e [flake8-pyi] Make PEP-695 functions with multiple type parameters fixable by PYI019 again (#15938) 2025-02-04 14:38:22 +00:00
David Peter
24c1cf71cb [red-knot] Use unambiguous invalid-syntax-construct for suppression comment test (#15933)
## Summary

I experimented with [not trimming trailing newlines in code
snippets](https://github.com/astral-sh/ruff/pull/15926#discussion_r1940992090),
but since came to the conclusion that the current behavior is better
because otherwise, there is no way to write snippets without a trailing
newline at all. And when you copy the code from a Markdown snippet in
GitHub, you also don't get a trailing newline.

I was surprised to see some test failures when I played with this
though, and decided to make this test independent from this
implementation detail.
2025-02-04 15:24:50 +01:00
Alex Waygood
f23802e219 Make Binding::range() point to the range of a type parameter's name, not the full type parameter (#15935)
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-02-04 14:14:21 +00:00
Micha Reiser
ff87ea8d42 Update black deviations (#15928) 2025-02-04 14:04:24 +00:00
David Peter
cc60701b59 [red-knot] MDTest: Fix line numbers in error messages (#15932)
## Summary

Fix line number reporting in MDTest error messages.

## Test Plan

Introduced an error in a Markdown test and made sure that the line in
the error message matches.
2025-02-04 13:44:05 +00:00
Brent Westbrook
b5e5271adf Preserve triple quotes and prefixes for strings (#15818)
## Summary

This is a follow-up to #15726, #15778, and #15794 to preserve the triple
quote and prefix flags in plain strings, bytestrings, and f-strings.

I also added a `StringLiteralFlags::without_triple_quotes` method to
avoid passing along triple quotes in rules like SIM905 where it might
not make sense, as discussed
[here](https://github.com/astral-sh/ruff/pull/15726#discussion_r1930532426).

## Test Plan

Existing tests, plus many new cases in the `generator::tests::quote`
test that should cover all combinations of quotes and prefixes, at least
for simple string bodies.

Closes #7799 when combined with #15694, #15726, #15778, and #15794.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-04 08:41:06 -05:00
David Peter
9a33924a65 [red-knot] Hand-written MDTest parser (#15926)
## Summary

Replaces our existing Markdown test parser with a fully hand-written
parser. I tried to fix this bug using the old approach and kept running
into problems. Eventually this seemed like the easier way. It's more
code (+50 lines, excluding the new test), but I hope it's relatively
straightforward to understand, compared to the complex interplay between
the byte-stream-manipulation and regex-parsing that we had before.

I did not really focus on performance, as the parsing time does not
dominate the test execution time, but this seems to be slightly faster
than what we had before (executing all MD tests; debug):

| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| this branch | 2.775 ± 0.072 | 2.690 | 2.877 | 1.00 |
| `main` | 2.921 ± 0.034 | 2.865 | 2.967 | 1.05 ± 0.03 |

closes #15923

## Test Plan

One new regression test.
2025-02-04 14:01:53 +01:00
Mike Perlov
15dd3b5ebd [pylint] Fix missing parens in unsafe fix for unnecessary-dunder-call (PLC2801) (#15762)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-04 09:54:01 +00:00
Alexander Nordin
b848afeae8 nit: docs for ignore & select (#15883) 2025-02-04 10:05:41 +01:00
Wei Lee
de4d9979eb [airflow] BashOperator has been moved to airflow.providers.standard.operators.bash.BashOperator (AIR302) (#15922)
## Summary

Extend AIR302 with 

* `airflow.operators.bash.BashOperator →
airflow.providers.standard.operators.bash.BashOperator`
* change existing rules `airflow.operators.bash_operator.BashOperator →
airflow.operators.bash.BashOperator` to
`airflow.operators.bash_operator.BashOperator →
airflow.providers.standard.operators.bash.BashOperator`

## Test Plan

a test fixture has been updated
2025-02-04 14:28:00 +05:30
InSync
ba02294af3 [flake8-logging] .exception() and exc_info= outside exception handlers (LOG004, LOG014) (#15799) 2025-02-04 09:52:12 +01:00
InSync
11cfe2ea8a [red-knot] Enforce specifying paths for mdtest code blocks in a separate preceding line (#15890)
## Summary

Resolves #15695, rework of #15704.

This change modifies the Mdtests framework so that:

* Paths must now be specified in a separate preceding line:

	`````markdown
	`a.py`:

	```py
	x = 1
	```
	`````

If the path of a file conflicts with its `lang`, an error will be
thrown.

* Configs are no longer accepted. The pattern still take them into
account, however, to avoid "Unterminated code block" errors.
* Unnamed files are now assigned unique, `lang`-respecting paths
automatically.

Additionally, all legacy usages have been updated.

## Test Plan

Unit tests and Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-02-04 08:27:17 +01:00
Douglas Creager
0529ad67d7 [red-knot] Internal refactoring of visibility constraints API (#15913)
This extracts some pure refactoring noise from
https://github.com/astral-sh/ruff/pull/15861. This changes the API for
creating and evaluating visibility constraints, but does not change how
they are respresented internally. There should be no behavioral or
performance changes in this PR.

Changes:

- Hide the internal representation isn't changed, so that we can make
changes to it in #15861.
- Add a separate builder type for visibility constraints. (With TDDs, we
will have some additional builder state that we can throw away once
we're done constructing.)
- Remove a layer of helper methods from `UseDefMapBuilder`, making
`SemanticIndexBuilder` responsible for constructing whatever visibility
constraints it needs.
2025-02-03 15:13:09 -05:00
David Peter
102c2eec12 [red-knot] Implicit instance attributes (#15811)
## Summary

Add support for implicitly-defined instance attributes, i.e. support
type inference for cases like this:
```py
class C:
    def __init__(self) -> None:
        self.x: int = 1
        self.y = None

reveal_type(C().x)  # int
reveal_type(C().y)  # Unknown | None
```

## Benchmarks

Codspeed reports no change in a cold-cache benchmark, and a -1%
regression in the incremental benchmark. On `black`'s `src` folder, I
don't see a statistically significant difference between the branches:

| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `./red_knot_main check --project /home/shark/black/src` | 133.7 ± 9.5 | 126.7 | 164.7 | 1.01 ± 0.08 |
| `./red_knot_feature check --project /home/shark/black/src` | 132.2 ± 5.1 | 118.1 | 140.9 | 1.00 |

## Test Plan

Updated and new Markdown tests
2025-02-03 19:34:23 +01:00
Justin Bramley
dc5e922221 [flake8-comprehensions] Handle extraneous parentheses around list comprehension (C403) (#15877)
## Summary

Given the following code:

```python
set(([x for x in range(5)]))
```

the current implementation of C403 results in

```python
{(x for x in range(5))}
```

which is a set containing a generator rather than the result of the
generator.

This change removes the extraneous parentheses so that the resulting
code is:

```python
{x for x in range(5)}
```


## Test Plan

`cargo nextest run` and `cargo insta test`
2025-02-03 13:26:03 -05:00
Alex Waygood
62075afe4f [flake8-pyi] Significantly improve accuracy of PYI019 if preview mode is enabled (#15888) 2025-02-03 15:45:10 +00:00
InSync
dfe1b849d0 Convert .md links in rule documentation to full URLs (#15904) 2025-02-03 15:33:03 +01:00
Alex Waygood
9c64d65552 [flake8-pyi] Rename PYI019 and improve its diagnostic message (#15885) 2025-02-03 14:23:58 +00:00
Vasco Schiavo
83243de93d Improve Docs: Pylint subcategories' codes (#15909) 2025-02-03 13:53:36 +01:00
renovate[bot]
638186afbd Update Rust crate rand to 0.9.0 (#15899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-03 12:25:57 +01:00
Dhruv Manilawala
d082c1b202 [red-knot] Add missing imports in mdtests (#15869)
## Summary

Related to #15848, this PR adds the imports explicitly as we'll now flag
these symbols as undefined.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-03 09:27:29 +00:00
InSync
30d5e9a2af [red-knot] Support --exit-zero and --error-on-warning (#15746)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-03 07:35:30 +00:00
renovate[bot]
a613345274 Update Rust crate imara-diff to v0.1.8 (#15891)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [imara-diff](https://redirect.github.com/pascalkuthe/imara-diff) |
workspace.dependencies | patch | `0.1.7` -> `0.1.8` |

---

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

---

### Release Notes

<details>
<summary>pascalkuthe/imara-diff (imara-diff)</summary>

###
[`v0.1.8`](https://redirect.github.com/pascalkuthe/imara-diff/blob/HEAD/CHANGELOG.md#018---2025-2-1)

[Compare
Source](https://redirect.github.com/pascalkuthe/imara-diff/compare/v0.1.7...v0.1.8)

##### Changed

-   update MSRV to 1.71
- drop ahash dependency in favour of hashbrowns default hasher
(foldhash)

##### Fixed

-   incomplete documentation for sink and interner

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 12:29:53 +05:30
renovate[bot]
c81f6c0bd2 Update dependency mdformat to v0.7.22 (#15896)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [mdformat](https://redirect.github.com/hukkin/mdformat)
([changelog](https://mdformat.readthedocs.io/en/stable/users/changelog.html))
| `==0.7.21` -> `==0.7.22` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/mdformat/0.7.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/mdformat/0.7.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/mdformat/0.7.21/0.7.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mdformat/0.7.21/0.7.22?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>hukkin/mdformat (mdformat)</summary>

###
[`v0.7.22`](https://redirect.github.com/hukkin/mdformat/compare/0.7.21...0.7.22)

[Compare
Source](https://redirect.github.com/hukkin/mdformat/compare/0.7.21...0.7.22)

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 12:24:31 +05:30
renovate[bot]
ba534d1931 Update Rust crate serde_json to v1.0.138 (#15893)
This PR contains the following updates:

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

---

> [!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.138`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.138)

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

-   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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNDUuMCIsInVwZGF0ZWRJblZlciI6IjM5LjE0NS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 12:22:32 +05:30
renovate[bot]
7e1db01041 Update Rust crate indicatif to v0.17.11 (#15892)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [indicatif](https://redirect.github.com/console-rs/indicatif) |
workspace.dependencies | patch | `0.17.9` -> `0.17.11` |

---

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

---

### Release Notes

<details>
<summary>console-rs/indicatif (indicatif)</summary>

###
[`v0.17.11`](https://redirect.github.com/console-rs/indicatif/releases/tag/0.17.11)

[Compare
Source](https://redirect.github.com/console-rs/indicatif/compare/0.17.10...0.17.11)

#### What's Changed

- Change `OnceCell` to `OnceLock` in `TabExpandedString` by
[@&#8203;tgross35](https://redirect.github.com/tgross35) in
[https://github.com/console-rs/indicatif/pull/694](https://redirect.github.com/console-rs/indicatif/pull/694)

###
[`v0.17.10`](https://redirect.github.com/console-rs/indicatif/releases/tag/0.17.10)

[Compare
Source](https://redirect.github.com/console-rs/indicatif/compare/0.17.9...0.17.10)

#### What's Changed

With some great performance improvements from
[@&#8203;jaheba](https://redirect.github.com/jaheba).

- Fix bar-less text output by
[@&#8203;spoutn1k](https://redirect.github.com/spoutn1k) in
[https://github.com/console-rs/indicatif/pull/659](https://redirect.github.com/console-rs/indicatif/pull/659)
- add tracing-indicatif create to integration list by
[@&#8203;emersonford](https://redirect.github.com/emersonford) in
[https://github.com/console-rs/indicatif/pull/673](https://redirect.github.com/console-rs/indicatif/pull/673)
- Fix double prints by
[@&#8203;spoutn1k](https://redirect.github.com/spoutn1k) in
[https://github.com/console-rs/indicatif/pull/671](https://redirect.github.com/console-rs/indicatif/pull/671)
- Only get draw_target-width when we actually draw by
[@&#8203;jaheba](https://redirect.github.com/jaheba) in
[https://github.com/console-rs/indicatif/pull/683](https://redirect.github.com/console-rs/indicatif/pull/683)
- Make tab extension lazy by
[@&#8203;jaheba](https://redirect.github.com/jaheba) in
[https://github.com/console-rs/indicatif/pull/684](https://redirect.github.com/console-rs/indicatif/pull/684)
- Make `ProgressBar:set_tab_with` take `&self` by
[@&#8203;jaheba](https://redirect.github.com/jaheba) in
[https://github.com/console-rs/indicatif/pull/685](https://redirect.github.com/console-rs/indicatif/pull/685)
- Remove unnecessary spinner display in multi examples by
[@&#8203;shuntaka9576](https://redirect.github.com/shuntaka9576) in
[https://github.com/console-rs/indicatif/pull/682](https://redirect.github.com/console-rs/indicatif/pull/682)
- Add `dec` and `dec_length` to `ProgressBar` by
[@&#8203;jaheba](https://redirect.github.com/jaheba) in
[https://github.com/console-rs/indicatif/pull/690](https://redirect.github.com/console-rs/indicatif/pull/690)
- Update rand requirement from 0.8 to 0.9 by
[@&#8203;dependabot](https://redirect.github.com/dependabot) in
[https://github.com/console-rs/indicatif/pull/693](https://redirect.github.com/console-rs/indicatif/pull/693)

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 12:22:11 +05:30
renovate[bot]
6331dd6272 Update Rust crate syn to v2.0.98 (#15894)
This PR contains the following updates:

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

---

> [!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.98`](https://redirect.github.com/dtolnay/syn/releases/tag/2.0.98)

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

- Allow lifetimes in function pointer return values in
`ParseStream::call` and `Punctuated` parsers
([#&#8203;1847](https://redirect.github.com/dtolnay/syn/issues/1847))

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

[Compare
Source](https://redirect.github.com/dtolnay/syn/compare/2.0.96...2.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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNDUuMCIsInVwZGF0ZWRJblZlciI6IjM5LjE0NS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 12:19:48 +05:30
renovate[bot]
4fe78db16c Update Rust crate unicode-ident to v1.0.16 (#15895)
This PR contains the following updates:

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

---

> [!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.16`](https://redirect.github.com/dtolnay/unicode-ident/releases/tag/1.0.16)

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

- Update `rand` dev dependency to 0.9
([#&#8203;29](https://redirect.github.com/dtolnay/unicode-ident/issues/29))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 12:18:59 +05:30
renovate[bot]
f5f74c95c5 Update Rust crate tempfile to v3.16.0 (#15900)
This PR contains the following updates:

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

---

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

---

### Release Notes

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

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

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

- Update `getrandom` to `0.3.0` (thanks to
[@&#8203;paolobarbolini](https://redirect.github.com/paolobarbolini)).
- Allow `windows-sys` versions `0.59.x` in addition to `0.59.0` (thanks
[@&#8203;ErichDonGubler](https://redirect.github.com/ErichDonGubler)).
- Improved security documentation (thanks to
[@&#8203;n0toose](https://redirect.github.com/n0toose) for collaborating
with me on this).

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 12:17:44 +05:30
renovate[bot]
464a893f5d Update pre-commit dependencies (#15898)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | patch | `v0.9.3` -> `v0.9.4` |
| [crate-ci/typos](https://redirect.github.com/crate-ci/typos) |
repository | patch | `v1.29.4` -> `v1.29.5` |
|
[executablebooks/mdformat](https://redirect.github.com/executablebooks/mdformat)
| repository | patch | `0.7.21` -> `0.7.22` |
|
[python-jsonschema/check-jsonschema](https://redirect.github.com/python-jsonschema/check-jsonschema)
| repository | patch | `0.31.0` -> `0.31.1` |
|
[woodruffw/zizmor-pre-commit](https://redirect.github.com/woodruffw/zizmor-pre-commit)
| repository | minor | `v1.2.2` -> `v1.3.0` |

---

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

Note: The `pre-commit` manager in Renovate is not supported by the
`pre-commit` maintainers or community. Please do not report any problems
there, instead [create a Discussion in the Renovate
repository](https://redirect.github.com/renovatebot/renovate/discussions/new)
if you have any questions.

---

### Release Notes

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

###
[`v0.9.4`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.9.4)

[Compare
Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.9.3...v0.9.4)

See: https://github.com/astral-sh/ruff/releases/tag/0.9.4

</details>

<details>
<summary>crate-ci/typos (crate-ci/typos)</summary>

###
[`v1.29.5`](https://redirect.github.com/crate-ci/typos/releases/tag/v1.29.5)

[Compare
Source](https://redirect.github.com/crate-ci/typos/compare/v1.29.4...v1.29.5)

#### \[1.29.5] - 2025-01-30

##### Internal

-   Update a dependency

</details>

<details>
<summary>executablebooks/mdformat (executablebooks/mdformat)</summary>

###
[`v0.7.22`](https://redirect.github.com/executablebooks/mdformat/compare/0.7.21...0.7.22)

[Compare
Source](https://redirect.github.com/executablebooks/mdformat/compare/0.7.21...0.7.22)

</details>

<details>
<summary>python-jsonschema/check-jsonschema
(python-jsonschema/check-jsonschema)</summary>

###
[`v0.31.1`](https://redirect.github.com/python-jsonschema/check-jsonschema/blob/HEAD/CHANGELOG.rst#0311)

[Compare
Source](https://redirect.github.com/python-jsonschema/check-jsonschema/compare/0.31.0...0.31.1)

- Update vendored schemas: buildkite, cloudbuild, compose-spec, mergify,
    renovate (2025-01-26)
-   Update the `gitlab` and `renovate` hooks to use
`--regex-variant nonunicode`. Thanks :user:`quentin-ag` and
:user:`Callek`
    for reporting! (:issue:`516`, :issue:`518`)
-   Update the required `ruamel.yaml` version to a range,
    `>=0.18.10,<0.19.0`.

</details>

<details>
<summary>woodruffw/zizmor-pre-commit
(woodruffw/zizmor-pre-commit)</summary>

###
[`v1.3.0`](https://redirect.github.com/woodruffw/zizmor-pre-commit/releases/tag/v1.3.0)

[Compare
Source](https://redirect.github.com/woodruffw/zizmor-pre-commit/compare/v1.2.2...v1.3.0)

See: https://github.com/woodruffw/zizmor/releases/tag/v1.3.0

</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.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- 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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNDUuMCIsInVwZGF0ZWRJblZlciI6IjM5LjE0NS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 00:32:32 +00:00
renovate[bot]
a53626a8b2 Update dependency ruff to v0.9.4 (#15897)
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.3` -> `==0.9.4` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.9.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/ruff/0.9.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/ruff/0.9.3/0.9.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.9.3/0.9.4?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.4`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#094)

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

##### Preview features

- \[`airflow`] Extend airflow context parameter check for
`BaseOperator.execute` (`AIR302`)
([#&#8203;15713](https://redirect.github.com/astral-sh/ruff/pull/15713))
- \[`airflow`] Update `AIR302` to check for deprecated context keys
([#&#8203;15144](https://redirect.github.com/astral-sh/ruff/pull/15144))
- \[`flake8-bandit`] Permit suspicious imports within stub files (`S4`)
([#&#8203;15822](https://redirect.github.com/astral-sh/ruff/pull/15822))
- \[`pylint`] Do not trigger `PLR6201` on empty collections
([#&#8203;15732](https://redirect.github.com/astral-sh/ruff/pull/15732))
- \[`refurb`] Do not emit diagnostic when loop variables are used
outside loop body (`FURB122`)
([#&#8203;15757](https://redirect.github.com/astral-sh/ruff/pull/15757))
- \[`ruff`] Add support for more `re` patterns (`RUF055`)
([#&#8203;15764](https://redirect.github.com/astral-sh/ruff/pull/15764))
- \[`ruff`] Check for shadowed `map` before suggesting fix (`RUF058`)
([#&#8203;15790](https://redirect.github.com/astral-sh/ruff/pull/15790))
- \[`ruff`] Do not emit diagnostic when all arguments to `zip()` are
variadic (`RUF058`)
([#&#8203;15744](https://redirect.github.com/astral-sh/ruff/pull/15744))
- \[`ruff`] Parenthesize fix when argument spans multiple lines for
`unnecessary-round` (`RUF057`)
([#&#8203;15703](https://redirect.github.com/astral-sh/ruff/pull/15703))

##### Rule changes

- Preserve quote style in generated code
([#&#8203;15726](https://redirect.github.com/astral-sh/ruff/pull/15726),
[#&#8203;15778](https://redirect.github.com/astral-sh/ruff/pull/15778),
[#&#8203;15794](https://redirect.github.com/astral-sh/ruff/pull/15794))
- \[`flake8-bugbear`] Exempt `NewType` calls where the original type is
immutable (`B008`)
([#&#8203;15765](https://redirect.github.com/astral-sh/ruff/pull/15765))
- \[`pylint`] Honor banned top-level imports by `TID253` in `PLC0415`.
([#&#8203;15628](https://redirect.github.com/astral-sh/ruff/pull/15628))
- \[`pyupgrade`] Ignore `is_typeddict` and `TypedDict` for
`deprecated-import` (`UP035`)
([#&#8203;15800](https://redirect.github.com/astral-sh/ruff/pull/15800))

##### CLI

- Fix formatter warning message for `flake8-quotes` option
([#&#8203;15788](https://redirect.github.com/astral-sh/ruff/pull/15788))
- Implement tab autocomplete for `ruff config`
([#&#8203;15603](https://redirect.github.com/astral-sh/ruff/pull/15603))

##### Bug fixes

- \[`flake8-comprehensions`] Do not emit `unnecessary-map` diagnostic
when lambda has different arity (`C417`)
([#&#8203;15802](https://redirect.github.com/astral-sh/ruff/pull/15802))
- \[`flake8-comprehensions`] Parenthesize `sorted` when needed for
`unnecessary-call-around-sorted` (`C413`)
([#&#8203;15825](https://redirect.github.com/astral-sh/ruff/pull/15825))
- \[`pyupgrade`] Handle end-of-line comments for `quoted-annotation`
(`UP037`)
([#&#8203;15824](https://redirect.github.com/astral-sh/ruff/pull/15824))

##### Documentation

- Add missing config docstrings
([#&#8203;15803](https://redirect.github.com/astral-sh/ruff/pull/15803))
- Add references to `trio.run_process` and `anyio.run_process`
([#&#8203;15761](https://redirect.github.com/astral-sh/ruff/pull/15761))
- Use `uv init --lib` in tutorial
([#&#8203;15718](https://redirect.github.com/astral-sh/ruff/pull/15718))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 00:31:02 +00:00
Alex Waygood
b08ce5fb18 [flake8-pyi] Minor cosmetic changes to PYI019 (#15881) 2025-02-02 19:20:05 +00:00
Alex Waygood
418aa35041 [flake8-pyi] Avoid an unnecessary .unwrap() call in PYI019 autofix (#15880) 2025-02-02 19:04:41 +00:00
Tom Kuson
813a76e9e2 [red-knot] Add version command (#15823)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-02 18:56:51 +00:00
InSync
3c09100484 [flake8-pyi] Fix more complex cases (PYI019) (#15821)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-02 18:38:49 +00:00
Micha Reiser
770b7f3439 Vendor benchmark test files (#15878)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-02 18:16:07 +00:00
Alex Waygood
d9a1034db0 Add convenience helper methods for AST nodes representing function parameters (#15871) 2025-02-01 17:16:32 +00:00
Alex Waygood
bcdb3f9840 Use Diagnostic::try_set_fix in bad-generator-return-type (#15873) 2025-02-01 15:44:42 +00:00
github-actions[bot]
942d7f395a Sync vendored typeshed stubs (#15864)
Close and reopen this PR to trigger CI

Co-authored-by: typeshedbot <>
2025-02-01 01:01:58 +00:00
Andrew Gallant
b58f2c399e [red-knot] ruff_db: make diagnostic rendering prettier (#15856)
This change does a simple swap of the existing renderer for one that
uses our vendored copy of `annotate-snippets`. We don't change anything
about the diagnostic data model, but this alone already makes
diagnostics look a lot nicer!
2025-01-31 16:37:02 -05:00
Douglas Creager
fab86de3ef [red-knot] Should A ∧ !A always be false? (#15839)
This mimics a simplification we have on the OR side, where we simplify
`A ∨ !A` to true. This requires changes to how we add `while` statements
to the semantic index, since we now need distinct
`VisibilityConstraint`s if we need to model evaluating a `Constraint`
multiple times at different points in the execution of the program.
2025-01-31 14:06:52 -05:00
Alex Waygood
c5c0b724fb [flake8-pyi] Minor simplification for PYI019 (#15855) 2025-01-31 16:54:38 +00:00
Alex Waygood
0d191a13c1 [flake8-pyi] Fix incorrect behaviour of custom-typevar-return-type preview-mode autofix if typing was already imported (PYI019) (#15853) 2025-01-31 16:46:31 +00:00
InSync
b2cb757fa8 [flake8-pyi] Remove type parameter correctly when it is the last (PYI019) (#15854) 2025-01-31 16:22:54 +00:00
Carl Meyer
ce769f6ae2 [red-knot] gather type prevalence statistics (#15834)
Something Alex and I threw together during our 1:1 this morning. Allows
us to collect statistics on the prevalence of various types in a file,
most usefully TODO types or other dynamic types.
2025-01-31 07:10:00 -08:00
Alex Waygood
44ac17b3ba [flake8-pyi] Fix several correctness issues with custom-type-var-return-type (PYI019) (#15851) 2025-01-31 14:19:35 +00:00
Brent Westbrook
f1418be81c [pyupgrade] Reuse replacement logic from UP046 and UP047 (UP040) (#15840)
## Summary

This is a follow-up to #15565, tracked in #15642, to reuse the string
replacement logic from the other PEP 695 rules instead of the
`Generator`, which has the benefit of preserving more comments. However,
comments in some places are still dropped, so I added a check for this
and update the fix safety accordingly. I also added a `## Fix safety`
section to the docs to reflect this and the existing `isinstance`
caveat.

## Test Plan

Existing UP040 tests, plus some new cases.
2025-01-31 08:10:53 -05:00
InSync
59be5f5278 [refurb] Avoid None | None as well as better detection and fix (FURB168) (#15779) 2025-01-31 11:34:57 +00:00
Dhruv Manilawala
4df0796d61 Remove non-existing lint.extendIgnore editor setting (#15844)
This setting doesn't exist in the first place. I must've added it by
mistake thinking that it exists similar to `extendSelect`. One reason to
have auto-generated docs.


988be01fbe/crates/ruff_server/src/session/settings.rs (L124-L133)

Closes: #14665
2025-01-31 06:00:17 +00:00
InSync
172f62d8f4 [refurb] Mark fix as unsafe if there are comments (FURB171) (#15832)
## Summary

Resolves #10063 and follow-up to #15521.

The fix is now marked as unsafe if there are any comments within its
range. Tests are adapted from that of #15521.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-01-30 17:21:07 -06:00
Dylan
071862af5a [flake8-comprehensions] Skip when TypeError present from too many (kw)args for C410,C411, and C418 (#15838)
Both `list` and `dict` expect only a single positional argument. Giving
more positional arguments, or a keyword argument, is a `TypeError` and
neither the lint rule nor its fix make sense in that context.

Closes #15810
2025-01-30 17:10:43 -06:00
Brent Westbrook
fe516e24f5 [pyflakes] Visit forward annotations in TypeAliasType as types (F401) (#15829)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/15812 by visiting the
second argument as a type definition.

## Test Plan

New F401 tests based on the report.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-30 18:06:38 -05:00
Dylan
4f2aea8d50 [flake8-comprehensions] Handle builtins at top of file correctly for unnecessary-dict-comprehension-for-iterable (C420) (#15837)
Builtin bindings are given a range of `0..0`, which causes strange
behavior when range checks are made at the top of the file. In this
case, the logic of the rule demands that the value of the dict
comprehension is not self-referential (i.e. it does not contain
definitions for any of the variables used within it). This logic was
confused by builtins which looked like they were defined "in the
comprehension", if the comprehension appeared at the top of the file.

Closes #15830
2025-01-30 15:49:13 -06:00
Dylan
5c77898693 Downgrade tailwind (#15835)
The new version of tailwindcss [sounds very
exciting](https://tailwindcss.com/blog/tailwindcss-v4), but upgrading
will requite some refactoring. For now, let's revert.
2025-01-30 13:55:07 -06:00
246 changed files with 14338 additions and 3952 deletions

View File

@@ -8,3 +8,7 @@ benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --"
# See: https://github.com/astral-sh/ruff/issues/11503
[target.'cfg(all(target_env="msvc", target_os = "windows"))']
rustflags = ["-C", "target-feature=+crt-static"]
[target.'wasm32-unknown-unknown']
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
rustflags = ["--cfg", 'getrandom_backend="wasm_js"']

View File

@@ -5,6 +5,7 @@ exclude: |
.github/workflows/release.yml|
crates/red_knot_vendored/vendor/.*|
crates/red_knot_project/resources/.*|
crates/ruff_benchmark/resources/.*|
crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*|
crates/ruff_notebook/resources/.*|
@@ -23,7 +24,7 @@ repos:
- id: validate-pyproject
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.21
rev: 0.7.22
hooks:
- id: mdformat
additional_dependencies:
@@ -59,7 +60,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
rev: v1.29.4
rev: v1.29.5
hooks:
- id: typos
@@ -73,7 +74,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.3
rev: v0.9.4
hooks:
- id: ruff-format
- id: ruff
@@ -91,12 +92,12 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.2.2
rev: v1.3.0
hooks:
- id: zizmor
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.31.0
rev: 0.31.1
hooks:
- id: check-github-workflows

368
Cargo.lock generated
View File

@@ -15,10 +15,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -190,12 +189,6 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "1.3.3"
@@ -245,9 +238,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.16.0"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "byteorder"
@@ -290,9 +283,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.10"
version = "1.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf"
dependencies = [
"jobserver",
"libc",
@@ -384,9 +377,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.42"
version = "4.5.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0"
checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6"
dependencies = [
"clap",
]
@@ -421,7 +414,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -710,7 +703,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -721,7 +714,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -791,7 +784,7 @@ dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -823,7 +816,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -846,9 +839,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
[[package]]
name = "either"
@@ -979,6 +972,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@@ -1034,10 +1033,24 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "glob"
version = "0.3.2"
@@ -1092,6 +1105,10 @@ name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"equivalent",
"foldhash",
]
[[package]]
name = "hashlink"
@@ -1282,7 +1299,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -1330,12 +1347,11 @@ dependencies = [
[[package]]
name = "imara-diff"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01"
checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2"
dependencies = [
"ahash",
"hashbrown 0.14.5",
"hashbrown 0.15.2",
]
[[package]]
@@ -1361,9 +1377,9 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.17.9"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console",
"number_prefix",
@@ -1448,7 +1464,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -1590,7 +1606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf66548c351bcaed792ef3e2b430cc840fbde504e09da6b29ed114ca60dcd4b"
dependencies = [
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -1754,7 +1770,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -2080,7 +2096,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -2120,7 +2136,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
"rand 0.8.5",
]
[[package]]
@@ -2134,22 +2150,22 @@ dependencies = [
[[package]]
name = "pin-project"
version = "1.1.8"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916"
checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.8"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb"
checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -2176,7 +2192,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -2269,7 +2285,7 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"rand",
"rand 0.8.5",
]
[[package]]
@@ -2299,8 +2315,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.0",
"zerocopy 0.8.14",
]
[[package]]
@@ -2310,7 +2337,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.0",
]
[[package]]
@@ -2319,7 +2356,17 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.14",
]
[[package]]
@@ -2503,6 +2550,7 @@ version = "0.0.0"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.1",
"js-sys",
"log",
"red_knot_project",
@@ -2528,7 +2576,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"getrandom 0.2.15",
"libredox",
"thiserror 1.0.69",
]
@@ -2577,28 +2625,13 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "ron"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
"base64 0.13.1",
"base64",
"bitflags 1.3.2",
"serde",
]
@@ -2692,11 +2725,7 @@ dependencies = [
"ruff_python_parser",
"ruff_python_trivia",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"tikv-jemallocator",
"ureq",
"url",
]
[[package]]
@@ -2717,6 +2746,7 @@ name = "ruff_db"
version = "0.0.0"
dependencies = [
"camino",
"colored 3.0.0",
"countme",
"dashmap 6.1.0",
"dunce",
@@ -2726,6 +2756,7 @@ dependencies = [
"insta",
"matchit",
"path-slash",
"ruff_annotate_snippets",
"ruff_cache",
"ruff_notebook",
"ruff_python_ast",
@@ -2907,7 +2938,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -2916,7 +2947,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
"rand",
"rand 0.9.0",
"ruff_diagnostics",
"ruff_source_file",
"ruff_text_size",
@@ -2968,6 +2999,7 @@ dependencies = [
"ruff_python_parser",
"ruff_source_file",
"ruff_text_size",
"test-case",
]
[[package]]
@@ -3160,6 +3192,7 @@ version = "0.9.4"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.1",
"js-sys",
"log",
"ruff_formatter",
@@ -3251,38 +3284,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.23.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.19"
@@ -3291,9 +3292,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "salsa"
@@ -3328,7 +3329,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
"synstructure",
]
@@ -3362,7 +3363,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -3405,7 +3406,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -3416,14 +3417,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
name = "serde_json"
version = "1.0.137"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"itoa",
"memchr",
@@ -3439,7 +3440,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -3480,7 +3481,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -3565,12 +3566,6 @@ dependencies = [
"anstream",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -3617,15 +3612,9 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
@@ -3639,9 +3628,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.96"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
@@ -3656,18 +3645,18 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
name = "tempfile"
version = "3.15.0"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [
"cfg-if",
"fastrand",
"getrandom",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.52.0",
@@ -3728,7 +3717,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -3739,7 +3728,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
"test-case-core",
]
@@ -3769,7 +3758,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -3780,7 +3769,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -3880,9 +3869,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.22"
version = "0.22.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
dependencies = [
"indexmap",
"serde",
@@ -3911,7 +3900,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -4061,9 +4050,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.15"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "unicode-normalization"
@@ -4105,7 +4094,7 @@ dependencies = [
"getopts",
"log",
"phf_codegen",
"rand",
"rand 0.8.5",
]
[[package]]
@@ -4114,28 +4103,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64 0.22.1",
"flate2",
"log",
"once_cell",
"rustls",
"rustls-pki-types",
"url",
"webpki-roots",
]
[[package]]
name = "url"
version = "2.5.4"
@@ -4184,8 +4151,8 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [
"getrandom",
"rand",
"getrandom 0.2.15",
"rand 0.8.5",
"uuid-macro-internal",
"wasm-bindgen",
]
@@ -4198,7 +4165,7 @@ checksum = "f8a86d88347b61a0e17b9908a67efcc594130830bf1045653784358dd023e294"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -4289,6 +4256,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
@@ -4311,7 +4287,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
"wasm-bindgen-shared",
]
@@ -4346,7 +4322,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4381,7 +4357,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
@@ -4404,15 +4380,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "which"
version = "7.0.1"
@@ -4624,9 +4591,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.24"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419"
dependencies = [
"memchr",
]
@@ -4637,6 +4604,15 @@ version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "write16"
version = "1.0.0"
@@ -4675,7 +4651,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
"synstructure",
]
@@ -4686,7 +4662,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
dependencies = [
"zerocopy-derive 0.8.14",
]
[[package]]
@@ -4697,7 +4682,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
@@ -4717,16 +4713,10 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zerovec"
version = "0.10.4"
@@ -4746,7 +4736,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.98",
]
[[package]]

View File

@@ -74,11 +74,13 @@ env_logger = { version = "0.11.0" }
etcetera = { version = "0.8.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
globwalk = { version = "0.9.1" }
hashbrown = { version = "0.15.0", default-features = false, features = [
"raw-entry",
"equivalent",
"inline-more",
] }
ignore = { version = "0.4.22" }
@@ -116,7 +118,7 @@ proc-macro2 = { version = "1.0.79" }
pyproject-toml = { version = "0.13.4" }
quick-junit = { version = "0.5.0" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
rand = { version = "0.9.0" }
rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
@@ -134,7 +136,12 @@ serde_with = { version = "3.6.0", default-features = false, features = [
shellexpand = { version = "3.0.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.2" }
snapbox = { version = "0.6.0", features = ["diff", "term-svg", "cmd", "examples"] }
snapbox = { version = "0.6.0", features = [
"diff",
"term-svg",
"cmd",
"examples",
] }
static_assertions = "1.1.0"
strum = { version = "0.26.0", features = ["strum_macros"] }
strum_macros = { version = "0.26.0" }
@@ -159,7 +166,6 @@ unicode-ident = { version = "1.0.12" }
unicode-width = { version = "0.2.0" }
unicode_names2 = { version = "1.2.2" }
unicode-normalization = { version = "0.1.23" }
ureq = { version = "2.9.6" }
url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = [
"v4",
@@ -173,6 +179,10 @@ wasm-bindgen-test = { version = "0.3.42" }
wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom"]
[workspace.lints.rust]
unsafe_code = "warn"
unreachable_pub = "warn"
@@ -305,7 +315,11 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker"]
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi", "./publish-wasm"]
# Post-announce jobs to run in CI
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
post-announce-jobs = [
"./notify-dependents",
"./publish-docs",
"./publish-playground",
]
# Custom permissions for GitHub Jobs
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
# Whether to install an updater program

104
crates/red_knot/build.rs Normal file
View File

@@ -0,0 +1,104 @@
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
fn main() {
// The workspace root directory is not available without walking up the tree
// https://github.com/rust-lang/cargo/issues/3946
let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("..")
.join("..");
commit_info(&workspace_root);
#[allow(clippy::disallowed_methods)]
let target = std::env::var("TARGET").unwrap();
println!("cargo::rustc-env=RUST_HOST_TARGET={target}");
}
fn commit_info(workspace_root: &Path) {
// If not in a git repository, do not attempt to retrieve commit information
let git_dir = workspace_root.join(".git");
if !git_dir.exists() {
return;
}
if let Some(git_head_path) = git_head(&git_dir) {
println!("cargo:rerun-if-changed={}", git_head_path.display());
let git_head_contents = fs::read_to_string(git_head_path);
if let Ok(git_head_contents) = git_head_contents {
// The contents are either a commit or a reference in the following formats
// - "<commit>" when the head is detached
// - "ref <ref>" when working on a branch
// If a commit, checking if the HEAD file has changed is sufficient
// If a ref, we need to add the head file for that ref to rebuild on commit
let mut git_ref_parts = git_head_contents.split_whitespace();
git_ref_parts.next();
if let Some(git_ref) = git_ref_parts.next() {
let git_ref_path = git_dir.join(git_ref);
println!("cargo:rerun-if-changed={}", git_ref_path.display());
}
}
}
let output = match Command::new("git")
.arg("log")
.arg("-1")
.arg("--date=short")
.arg("--abbrev=9")
.arg("--format=%H %h %cd %(describe)")
.output()
{
Ok(output) if output.status.success() => output,
_ => return,
};
let stdout = String::from_utf8(output.stdout).unwrap();
let mut parts = stdout.split_whitespace();
let mut next = || parts.next().unwrap();
let _commit_hash = next();
println!("cargo::rustc-env=RED_KNOT_COMMIT_SHORT_HASH={}", next());
println!("cargo::rustc-env=RED_KNOT_COMMIT_DATE={}", next());
// Describe can fail for some commits
// https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem
if let Some(describe) = parts.next() {
let mut describe_parts = describe.split('-');
let _last_tag = describe_parts.next().unwrap();
// If this is the tagged commit, this component will be missing
println!(
"cargo::rustc-env=RED_KNOT_LAST_TAG_DISTANCE={}",
describe_parts.next().unwrap_or("0")
);
}
}
fn git_head(git_dir: &Path) -> Option<PathBuf> {
// The typical case is a standard git repository.
let git_head_path = git_dir.join("HEAD");
if git_head_path.exists() {
return Some(git_head_path);
}
if !git_dir.is_file() {
return None;
}
// If `.git/HEAD` doesn't exist and `.git` is actually a file,
// then let's try to attempt to read it as a worktree. If it's
// a worktree, then its contents will look like this, e.g.:
//
// gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2
//
// And the HEAD file we want to watch will be at:
//
// /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD
let contents = fs::read_to_string(git_dir).ok()?;
let (label, worktree_path) = contents.split_once(':')?;
if label != "gitdir" {
return None;
}
let worktree_path = worktree_path.trim();
Some(PathBuf::from(worktree_path))
}

View File

@@ -25,6 +25,9 @@ pub(crate) enum Command {
/// Start the language server
Server,
/// Display Red Knot's version
Version,
}
#[derive(Debug, Parser)]
@@ -63,6 +66,14 @@ pub(crate) struct CheckCommand {
#[clap(flatten)]
pub(crate) rules: RulesArg,
/// Use exit code 1 if there are any warning-level diagnostics.
#[arg(long, conflicts_with = "exit_zero")]
pub(crate) error_on_warning: bool,
/// Always use exit code 0, even when there are error-level diagnostics.
#[arg(long)]
pub(crate) exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(long, short = 'W')]
pub(crate) watch: bool,

View File

@@ -1,4 +1,7 @@
use std::io::{self, BufWriter, Write};
use std::process::{ExitCode, Termination};
use anyhow::Result;
use std::sync::Mutex;
use crate::args::{Args, CheckCommand, Command};
@@ -12,7 +15,7 @@ use red_knot_project::watch;
use red_knot_project::watch::ProjectWatcher;
use red_knot_project::{ProjectDatabase, ProjectMetadata};
use red_knot_server::run_server;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::diagnostic::{Diagnostic, Severity};
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
@@ -20,6 +23,7 @@ mod args;
mod logging;
mod python_version;
mod verbosity;
mod version;
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
pub fn main() -> ExitStatus {
@@ -49,9 +53,17 @@ fn run() -> anyhow::Result<ExitStatus> {
match args.command {
Command::Server => run_server().map(|()| ExitStatus::Success),
Command::Check(check_args) => run_check(check_args),
Command::Version => version().map(|()| ExitStatus::Success),
}
}
pub(crate) fn version() -> Result<()> {
let mut stdout = BufWriter::new(io::stdout().lock());
let version_info = crate::version::version();
writeln!(stdout, "red knot {}", &version_info)?;
Ok(())
}
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let verbosity = args.verbosity.level();
countme::enable(verbosity.is_trace());
@@ -84,13 +96,20 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let system = OsSystem::new(cwd);
let watch = args.watch;
let exit_zero = args.exit_zero;
let min_error_severity = if args.error_on_warning {
Severity::Warning
} else {
Severity::Error
};
let cli_options = args.into_options();
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
workspace_metadata.apply_cli_options(cli_options.clone());
let mut db = ProjectDatabase::new(workspace_metadata, system)?;
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options, min_error_severity);
// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -112,7 +131,11 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
std::mem::forget(db);
Ok(exit_status)
if exit_zero {
Ok(ExitStatus::Success)
} else {
Ok(exit_status)
}
}
#[derive(Copy, Clone)]
@@ -144,10 +167,18 @@ struct MainLoop {
watcher: Option<ProjectWatcher>,
cli_options: Options,
/// The minimum severity to consider an error when deciding the exit status.
///
/// TODO(micha): Get from the terminal settings.
min_error_severity: Severity,
}
impl MainLoop {
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
fn new(
cli_options: Options,
min_error_severity: Severity,
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
(
@@ -156,6 +187,7 @@ impl MainLoop {
receiver,
watcher: None,
cli_options,
min_error_severity,
},
MainLoopCancellationToken { sender },
)
@@ -213,7 +245,10 @@ impl MainLoop {
result,
revision: check_revision,
} => {
let has_diagnostics = !result.is_empty();
let failed = result
.iter()
.any(|diagnostic| diagnostic.severity() >= self.min_error_severity);
if check_revision == revision {
#[allow(clippy::print_stdout)]
for diagnostic in result {
@@ -226,7 +261,7 @@ impl MainLoop {
}
if self.watcher.is_none() {
return if has_diagnostics {
return if failed {
ExitStatus::Failure
} else {
ExitStatus::Success

View File

@@ -0,0 +1,105 @@
//! Code for representing Red Knot's release version number.
use std::fmt;
/// Information about the git repository where Red Knot was built from.
pub(crate) struct CommitInfo {
short_commit_hash: String,
commit_date: String,
commits_since_last_tag: u32,
}
/// Red Knot's version.
pub(crate) struct VersionInfo {
/// Red Knot's version, such as "0.5.1"
version: String,
/// Information about the git commit we may have been built from.
///
/// `None` if not built from a git repo or if retrieval failed.
commit_info: Option<CommitInfo>,
}
impl fmt::Display for VersionInfo {
/// Formatted version information: `<version>[+<commits>] (<commit> <date>)`
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.version)?;
if let Some(ref ci) = self.commit_info {
if ci.commits_since_last_tag > 0 {
write!(f, "+{}", ci.commits_since_last_tag)?;
}
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
}
Ok(())
}
}
/// Returns information about Red Knot's version.
pub(crate) fn version() -> VersionInfo {
// Environment variables are only read at compile-time
macro_rules! option_env_str {
($name:expr) => {
option_env!($name).map(|s| s.to_string())
};
}
// This version is pulled from Cargo.toml and set by Cargo
let version = option_env_str!("CARGO_PKG_VERSION").unwrap();
// Commit info is pulled from git and set by `build.rs`
let commit_info =
option_env_str!("RED_KNOT_COMMIT_SHORT_HASH").map(|short_commit_hash| CommitInfo {
short_commit_hash,
commit_date: option_env_str!("RED_KNOT_COMMIT_DATE").unwrap(),
commits_since_last_tag: option_env_str!("RED_KNOT_LAST_TAG_DISTANCE")
.as_deref()
.map_or(0, |value| value.parse::<u32>().unwrap_or(0)),
});
VersionInfo {
version,
commit_info,
}
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use super::{CommitInfo, VersionInfo};
#[test]
fn version_formatting() {
let version = VersionInfo {
version: "0.0.0".to_string(),
commit_info: None,
};
assert_snapshot!(version, @"0.0.0");
}
#[test]
fn version_formatting_with_commit_info() {
let version = VersionInfo {
version: "0.0.0".to_string(),
commit_info: Some(CommitInfo {
short_commit_hash: "53b0f5d92".to_string(),
commit_date: "2023-10-19".to_string(),
commits_since_last_tag: 0,
}),
};
assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)");
}
#[test]
fn version_formatting_with_commits_since_last_tag() {
let version = VersionInfo {
version: "0.0.0".to_string(),
commit_info: Some(CommitInfo {
short_commit_hash: "53b0f5d92".to_string(),
commit_date: "2023-10-19".to_string(),
commits_since_last_tag: 24,
}),
};
assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)");
}
}

View File

@@ -28,14 +28,21 @@ fn config_override() -> anyhow::Result<()> {
),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[lint:unresolved-attribute] <temp_dir>/test.py:5:7 Type `<module 'sys'>` has no attribute `last_exc`
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-attribute
--> <temp_dir>/test.py:5:7
|
4 | # Access `sys.last_exc` that was only added in Python 3.12
5 | print(sys.last_exc)
| ^^^^^^^^^^^^ Type `<module 'sys'>` has no attribute `last_exc`
|
----- stderr -----
");
----- stderr -----
"###);
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
success: true
@@ -91,14 +98,22 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
])?;
// Make sure that the CLI fails when the `libs` directory is not in the search path.
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r#"
success: false
exit_code: 1
----- stdout -----
error[lint:unresolved-import] <temp_dir>/child/test.py:2:1 Cannot resolve import `utils`
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r###"
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
--> <temp_dir>/child/test.py:2:1
|
2 | from utils import add
| ^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `utils`
3 |
4 | stat = add(10, 15)
|
----- stderr -----
"#);
----- stderr -----
"###);
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
success: true
@@ -180,15 +195,31 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
----- stderr -----
");
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| - Name `x` used when possibly not defined
|
----- stderr -----
"###);
case.write_file(
"pyproject.toml",
@@ -199,14 +230,22 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
----- stderr -----
");
----- stderr -----
"###);
Ok(())
}
@@ -230,16 +269,42 @@ fn cli_rule_severity() -> anyhow::Result<()> {
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[lint:unresolved-import] <temp_dir>/test.py:2:8 Cannot resolve import `does_not_exit`
error[lint:division-by-zero] <temp_dir>/test.py:4:5 Cannot divide object of type `Literal[4]` by zero
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:9:7 Name `x` used when possibly not defined
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
3 |
4 | y = 4 / 0
|
----- stderr -----
");
error: lint:division-by-zero
--> <temp_dir>/test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
5 |
6 | for a in range(0, y):
|
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:9:7
|
7 | x = a
8 |
9 | print(x) # possibly-unresolved-reference
| - Name `x` used when possibly not defined
|
----- stderr -----
"###);
assert_cmd_snapshot!(
case
@@ -250,15 +315,33 @@ fn cli_rule_severity() -> anyhow::Result<()> {
.arg("division-by-zero")
.arg("--warn")
.arg("unresolved-import"),
@r"
success: false
exit_code: 1
@r###"
success: true
exit_code: 0
----- stdout -----
warning[lint:unresolved-import] <temp_dir>/test.py:2:8 Cannot resolve import `does_not_exit`
warning[lint:division-by-zero] <temp_dir>/test.py:4:5 Cannot divide object of type `Literal[4]` by zero
warning: lint:unresolved-import
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ------------- Cannot resolve import `does_not_exit`
3 |
4 | y = 4 / 0
|
warning: lint:division-by-zero
--> <temp_dir>/test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
5 |
6 | for a in range(0, y):
|
----- stderr -----
"
"###
);
Ok(())
@@ -282,15 +365,31 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
----- stderr -----
");
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| - Name `x` used when possibly not defined
|
----- stderr -----
"###);
assert_cmd_snapshot!(
case
@@ -302,14 +401,22 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
// Override the error severity with warning
.arg("--ignore")
.arg("possibly-unresolved-reference"),
@r"
success: false
exit_code: 1
----- stdout -----
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
@r###"
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
----- stderr -----
"
----- stderr -----
"###
);
Ok(())
@@ -329,14 +436,21 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
("test.py", "print(10)"),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
warning[unknown-rule] <temp_dir>/pyproject.toml:3:1 Unknown lint rule `division-by-zer`
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
warning: unknown-rule
--> <temp_dir>/pyproject.toml:3:1
|
2 | [tool.knot.rules]
3 | division-by-zer = "warn" # incorrect rule name
| --------------- Unknown lint rule `division-by-zer`
|
----- stderr -----
");
----- stderr -----
"###);
Ok(())
}
@@ -346,14 +460,228 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
fn cli_unknown_rules() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", "print(10)")?;
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unknown-rule] Unknown lint rule `division-by-zer`
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r###"
success: true
exit_code: 0
----- stdout -----
warning: unknown-rule: Unknown lint rule `division-by-zer`
----- stderr -----
");
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_only_warnings() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_only_info() -> anyhow::Result<()> {
let case = TestCase::with_file(
"test.py",
r#"
from typing_extensions import reveal_type
reveal_type(1)
"#,
)?;
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
info: revealed-type
--> <temp_dir>/test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| -------------- info: Revealed type is `Literal[1]`
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
let case = TestCase::with_file(
"test.py",
r#"
from typing_extensions import reveal_type
reveal_type(1)
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
success: true
exit_code: 0
----- stdout -----
info: revealed-type
--> <temp_dir>/test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| -------------- info: Revealed type is `Literal[1]`
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
let case = TestCase::with_file(
"test.py",
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"#,
)?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> {
let case = TestCase::with_file(
"test.py",
r###"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"###,
)?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
let case = TestCase::with_file(
"test.py",
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r###"
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
----- stderr -----
"###);
Ok(())
}

View File

@@ -2,7 +2,9 @@
## Deferred annotations in stubs always resolve
```pyi path=mod.pyi
`mod.pyi`:
```pyi
def get_foo() -> Foo: ...
class Foo: ...
```

View File

@@ -116,7 +116,9 @@ def union_example(
Only Literal that is defined in typing and typing_extension modules is detected as the special
Literal.
```pyi path=other.pyi
`other.pyi`:
```pyi
from typing import _SpecialForm
Literal: _SpecialForm

View File

@@ -25,7 +25,9 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
## Tuple annotations are understood
```py path=module.py
`module.py`:
```py
from typing_extensions import Unpack
a: tuple[()] = ()
@@ -40,7 +42,9 @@ i: tuple[str | int, str | int] = (42, 42)
j: tuple[str | int] = (42,)
```
```py path=script.py
`script.py`:
```py
from module import a, b, c, d, e, f, g, h, i, j
reveal_type(a) # revealed: tuple[()]
@@ -114,7 +118,7 @@ reveal_type(x) # revealed: Foo
## Annotations in stub files are deferred
```pyi path=main.pyi
```pyi
x: Foo
class Foo: ...
@@ -125,7 +129,7 @@ reveal_type(x) # revealed: Foo
## Annotated assignments in stub files are inferred correctly
```pyi path=main.pyi
```pyi
x: int = 1
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -25,25 +25,21 @@ class C:
c_instance = C(1)
# TODO: Mypy/pyright infer `int | str` here. We want this to be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_value) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
# TODO: should be `int | None`
reveal_type(c_instance.inferred_from_param) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
# TODO: should be `bytes`
reveal_type(c_instance.declared_only) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.declared_only) # revealed: bytes
# TODO: should be `bool`
reveal_type(c_instance.declared_and_bound) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.declared_and_bound) # revealed: bool
# TODO: should be `str`
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound.
# mypy and pyright do not show an error here.
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`.
c_instance.inferred_from_value = "value set on instance"
@@ -71,7 +67,7 @@ c_instance.declared_and_bound = False
# in general (we don't know what else happened to `c_instance` between the assignment and the use
# here), but mypy and pyright support this. In conclusion, this could be `bool` but should probably
# be `Literal[False]`.
reveal_type(c_instance.declared_and_bound) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.declared_and_bound) # revealed: bool
```
#### Variable declared in class body and possibly bound in `__init__`
@@ -124,7 +120,44 @@ reveal_type(C.only_declared) # revealed: str
C.only_declared = "overwritten on class"
```
#### Variable only defined in unrelated method
#### Mixed declarations/bindings in class body and `__init__`
```py
class C:
only_declared_in_body: str | None
declared_in_body_and_init: str | None
declared_in_body_defined_in_init: str | None
bound_in_body_declared_in_init = "a"
bound_in_body_and_init = None
def __init__(self, flag) -> None:
self.only_declared_in_init: str | None
self.declared_in_body_and_init: str | None = None
self.declared_in_body_defined_in_init = "a"
self.bound_in_body_declared_in_init: str | None
if flag:
self.bound_in_body_and_init = "a"
c_instance = C(True)
reveal_type(c_instance.only_declared_in_body) # revealed: str | None
reveal_type(c_instance.only_declared_in_init) # revealed: str | None
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
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
```
#### Variable defined in non-`__init__` method
We also recognize pure instance variables if they are defined in a method that is not `__init__`.
@@ -143,20 +176,17 @@ class C:
c_instance = C(1)
# TODO: Should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_value) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
# TODO: Should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
# TODO: Should be `int | None`
reveal_type(c_instance.inferred_from_param) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
# TODO: Should be `bytes`
reveal_type(c_instance.declared_only) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.declared_only) # revealed: bytes
# TODO: Should be `bool`
reveal_type(c_instance.declared_and_bound) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.declared_and_bound) # revealed: bool
# TODO: We already show an error here, but the message might be improved?
# error: [unresolved-attribute]
@@ -166,6 +196,138 @@ reveal_type(C.inferred_from_value) # revealed: Unknown
C.inferred_from_value = "overwritten on class"
```
#### Variable defined in multiple methods
If we see multiple un-annotated assignments to a single attribute (`self.x` below), we build the
union of all inferred types (and `Unknown`). If we see multiple conflicting declarations of the same
attribute, that should be an error.
```py
def get_int() -> int:
return 0
def get_str() -> str:
return "a"
class C:
def __init__(self) -> None:
self.x = get_int()
self.y: int = 1
def other_method(self):
self.x = get_str()
# TODO: this redeclaration should be an error
self.y: str = "a"
c_instance = C()
reveal_type(c_instance.x) # revealed: Unknown | int | str
# TODO: We should probably infer `int | str` here.
reveal_type(c_instance.y) # revealed: int
```
#### Attributes defined in tuple unpackings
```py
def returns_tuple() -> tuple[int, str]:
return (1, "a")
class C:
a1, b1 = (1, "a")
c1, d1 = returns_tuple()
def __init__(self) -> None:
self.a2, self.b2 = (1, "a")
self.c2, self.d2 = returns_tuple()
c_instance = C()
reveal_type(c_instance.a1) # revealed: Unknown | Literal[1]
reveal_type(c_instance.b1) # revealed: Unknown | Literal["a"]
reveal_type(c_instance.c1) # revealed: Unknown | int
reveal_type(c_instance.d1) # revealed: Unknown | str
# TODO: This should be supported (no error; type should be: `Unknown | Literal[1]`)
# error: [unresolved-attribute]
reveal_type(c_instance.a2) # revealed: Unknown
# TODO: This should be supported (no error; type should be: `Unknown | Literal["a"]`)
# error: [unresolved-attribute]
reveal_type(c_instance.b2) # revealed: Unknown
# TODO: Similar for these two (should be `Unknown | int` and `Unknown | str`, respectively)
# error: [unresolved-attribute]
reveal_type(c_instance.c2) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(c_instance.d2) # revealed: Unknown
```
#### Attributes defined in for-loop (unpacking)
```py
class IntIterator:
def __next__(self) -> int:
return 1
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
class TupleIterator:
def __next__(self) -> tuple[int, str]:
return (1, "a")
class TupleIterable:
def __iter__(self) -> TupleIterator:
return TupleIterator()
class C:
def __init__(self):
for self.x in IntIterable():
pass
for _, self.y in TupleIterable():
pass
# TODO: Pyright fully supports these, mypy detects the presence of the attributes,
# but infers type `Any` for both of them. We should infer `int` and `str` here:
# error: [unresolved-attribute]
reveal_type(C().x) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(C().y) # revealed: Unknown
```
#### Conditionally declared / bound attributes
We currently do not raise a diagnostic or change behavior if an attribute is only conditionally
defined. This is consistent with what mypy and pyright do.
```py
def flag() -> bool:
return True
class C:
def f(self) -> None:
if flag():
self.a1: str | None = "a"
self.b1 = 1
if flag():
def f(self) -> None:
self.a2: str | None = "a"
self.b2 = 1
c_instance = C()
reveal_type(c_instance.a1) # revealed: str | None
reveal_type(c_instance.a2) # revealed: str | None
reveal_type(c_instance.b1) # revealed: Unknown | Literal[1]
reveal_type(c_instance.b2) # revealed: Unknown | Literal[1]
```
#### Methods that does not use `self` as a first parameter
```py
@@ -175,8 +337,7 @@ class C:
def __init__(this) -> None:
this.declared_and_bound: str | None = "a"
# TODO: should be `str | None`
reveal_type(C().declared_and_bound) # revealed: @Todo(implicit instance attribute)
reveal_type(C().declared_and_bound) # revealed: str | None
```
#### Aliased `self` parameter
@@ -187,9 +348,28 @@ class C:
this = self
this.declared_and_bound: str | None = "a"
# TODO: This would ideally be `str | None`, but mypy/pyright don't support this either,
# This would ideally be `str | None`, but mypy/pyright don't support this either,
# so `Unknown` + a diagnostic is also fine.
reveal_type(C().declared_and_bound) # revealed: @Todo(implicit instance attribute)
# error: [unresolved-attribute]
reveal_type(C().declared_and_bound) # revealed: Unknown
```
#### Attributes defined in statically-known-to-be-false branches
```py
class C:
def __init__(self) -> None:
# We use a "significantly complex" condition here (instead of just `False`)
# for a proper comparison with mypy and pyright, which distinguish between
# conditions that can be resolved from a simple pattern matching and those
# that need proper type inference.
if (2 + 3) < 4:
self.x: str = "a"
# TODO: Ideally, this would result in a `unresolved-attribute` error. But mypy and pyright
# do not support this either (for conditions that can only be resolved to `False` in type
# inference), so it does not seem to be particularly important.
reveal_type(C().x) # revealed: str
```
### Pure class variables (`ClassVar`)
@@ -266,7 +446,7 @@ reveal_type(C.pure_class_variable) # revealed: Unknown
c_instance = C()
# TODO: should be `Literal["overwritten on class"]`
reveal_type(c_instance.pure_class_variable) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
# TODO: should raise an error.
c_instance.pure_class_variable = "value set on instance"
@@ -360,8 +540,7 @@ reveal_type(Derived.declared_in_body) # revealed: int | None
reveal_type(Derived().declared_in_body) # revealed: int | None
# TODO: Should be `str | None`
reveal_type(Derived().defined_in_init) # revealed: @Todo(implicit instance attribute)
reveal_type(Derived().defined_in_init) # revealed: str | None
```
## Union of attributes
@@ -524,7 +703,9 @@ reveal_type(Foo.__class__) # revealed: Literal[type]
## Module attributes
```py path=mod.py
`mod.py`:
```py
global_symbol: str = "a"
```
@@ -558,13 +739,19 @@ for mod.global_symbol in IntIterable():
## Nested attributes
```py path=outer/__init__.py
`outer/__init__.py`:
```py
```
```py path=outer/nested/__init__.py
`outer/nested/__init__.py`:
```py
```
```py path=outer/nested/inner.py
`outer/nested/inner.py`:
```py
class Outer:
class Nested:
class Inner:
@@ -587,7 +774,9 @@ outer.nested.inner.Outer.Nested.Inner.attr = "a"
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
functions are instances of that class:
```py path=a.py
`a.py`:
```py
def f(): ...
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
@@ -596,7 +785,9 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
Some attributes are special-cased, however:
```py path=b.py
`b.py`:
```py
def f(): ...
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
@@ -608,14 +799,18 @@ reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
integers are instances of that class:
```py path=a.py
`a.py`:
```py
reveal_type((2).bit_length) # revealed: @Todo(bound method)
reveal_type((2).denominator) # revealed: @Todo(@property)
```
Some attributes are special-cased, however:
```py path=b.py
`b.py`:
```py
reveal_type((2).numerator) # revealed: Literal[2]
reveal_type((2).real) # revealed: Literal[2]
```
@@ -625,14 +820,18 @@ reveal_type((2).real) # revealed: Literal[2]
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
bols are instances of that class:
```py path=a.py
`a.py`:
```py
reveal_type(True.__and__) # revealed: @Todo(bound method)
reveal_type(False.__or__) # revealed: @Todo(bound method)
```
Some attributes are special-cased, however:
```py path=b.py
`b.py`:
```py
reveal_type(True.numerator) # revealed: Literal[1]
reveal_type(False.real) # revealed: Literal[0]
```
@@ -646,6 +845,90 @@ reveal_type(b"foo".join) # revealed: @Todo(bound method)
reveal_type(b"foo".endswith) # revealed: @Todo(bound method)
```
## Instance attribute edge cases
### Assignment to attribute that does not correspond to the instance
```py
class Other:
x: int = 1
class C:
def __init__(self, other: Other) -> None:
other.x = 1
def f(c: C):
# error: [unresolved-attribute]
reveal_type(c.x) # revealed: Unknown
```
### Nested classes
```py
class Outer:
def __init__(self):
self.x: int = 1
class Middle:
# has no 'x' attribute
class Inner:
def __init__(self):
self.x: str = "a"
reveal_type(Outer().x) # revealed: int
# error: [unresolved-attribute]
Outer.Middle().x
reveal_type(Outer.Middle.Inner().x) # revealed: str
```
### Shadowing of `self`
```py
class Other:
x: int = 1
class C:
def __init__(self) -> None:
# Redeclaration of self. `self` does not refer to the instance anymore.
self: Other = Other()
self.x: int = 1
# TODO: this should be an error
C().x
```
### Assignment to `self` after nested function
```py
class Other:
x: str = "a"
class C:
def __init__(self) -> None:
def nested_function(self: Other):
self.x = "b"
self.x: int = 1
reveal_type(C().x) # revealed: int
```
### Assignment to `self` from nested function
```py
class C:
def __init__(self) -> None:
def set_attribute(value: str):
self.x: str = value
set_attribute("a")
# TODO: ideally, this would be `str`. Mypy supports this, pyright does not.
# error: [unresolved-attribute]
reveal_type(C().x) # revealed: Unknown
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View File

@@ -3,6 +3,8 @@
## Class instances
```py
from typing import Literal
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
@@ -136,6 +138,8 @@ reveal_type(No() // Yes()) # revealed: Unknown
## Subclass reflections override superclass dunders
```py
from typing import Literal
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
@@ -294,6 +298,8 @@ itself. (For these operators to work on the class itself, they would have to be
class's type, i.e. `type`.)
```py
from typing import Literal
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
@@ -312,6 +318,8 @@ reveal_type(No + No) # revealed: Unknown
## Subclass
```py
from typing import Literal
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"

View File

@@ -36,7 +36,9 @@ In particular, we should raise errors in the "possibly-undeclared-and-unbound" a
If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type
(`Literal[1]`), or a conflicting inferred type (`str` vs. `Literal[2]` below):
```py path=mod.py
`mod.py`:
```py
from typing import Any
def any() -> Any: ...
@@ -61,7 +63,9 @@ reveal_type(d) # revealed: int
If a symbol is declared and *possibly* unbound, we trust that other module and use the declared type
without raising an error.
```py path=mod.py
`mod.py`:
```py
from typing import Any
def any() -> Any: ...
@@ -93,7 +97,9 @@ reveal_type(d) # revealed: int
Similarly, if a symbol is declared but unbound, we do not raise an error. We trust that this symbol
is available somehow and simply use the declared type.
```py path=mod.py
`mod.py`:
```py
from typing import Any
a: int
@@ -114,7 +120,9 @@ reveal_type(b) # revealed: Any
If a symbol is possibly undeclared but definitely bound, we use the union of the declared and
inferred types:
```py path=mod.py
`mod.py`:
```py
from typing import Any
def any() -> Any: ...
@@ -151,7 +159,11 @@ inferred types. This case is interesting because the "possibly declared" definit
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import`
error for both `a` and `b`:
```py path=mod.py
`mod.py`:
```py
from typing import Any
def flag() -> bool: ...
if flag():
@@ -179,7 +191,9 @@ b = None
If a symbol is possibly undeclared and definitely unbound, we currently do not raise an error. This
seems inconsistent when compared to the case just above.
```py path=mod.py
`mod.py`:
```py
def flag() -> bool: ...
if flag():
@@ -206,7 +220,9 @@ If a symbol is *undeclared*, we use the union of `Unknown` with the inferred typ
treat this case differently from the case where a symbol is implicitly declared with `Unknown`,
possibly due to the usage of an unknown name in the annotation:
```py path=mod.py
`mod.py`:
```py
# Undeclared:
a = 1
@@ -229,7 +245,9 @@ a = None
If a symbol is undeclared and *possibly* unbound, we currently do not raise an error. This seems
inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case.
```py path=mod.py
`mod.py`:
```py
def flag() -> bool: ...
if flag:
@@ -253,7 +271,9 @@ a = None
If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error.
```py path=mod.py
`mod.py`:
```py
if False:
a: int = 1
```

View File

@@ -6,6 +6,8 @@ If we have an intersection type `A & B` and we get a definitive true/false answe
types, we can infer that the result for the intersection type is also true/false:
```py
from typing import Literal
class Base: ...
class Child1(Base):

View File

@@ -33,7 +33,9 @@ reveal_type(a >= b) # revealed: Literal[False]
Even when tuples have different lengths, comparisons should be handled appropriately.
```py path=different_length.py
`different_length.py`:
```py
a = (1, 2, 3)
b = (1, 2, 3, 4)
@@ -102,7 +104,9 @@ reveal_type(a >= b) # revealed: bool
However, if the lexicographic comparison completes without reaching a point where str and int are
compared, Python will still produce a result based on the prior elements.
```py path=short_circuit.py
`short_circuit.py`:
```py
a = (1, 2)
b = (999999, "hello")

View File

@@ -78,7 +78,7 @@ def _(a: type[Unknown], b: type[Any]):
Tuple types with the same elements are the same.
```py
from typing_extensions import assert_type
from typing_extensions import Any, assert_type
from knot_extensions import Unknown

View File

@@ -29,7 +29,9 @@ completing. The type of `x` at the beginning of the `except` suite in this examp
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
*after* that redefinition.
```py path=union_type_inferred.py
`union_type_inferred.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -50,7 +52,9 @@ reveal_type(x) # revealed: str | Literal[2]
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
inferred as having a union type following the `try`/`except` block:
```py path=branches_unify_to_non_union_type.py
`branches_unify_to_non_union_type.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -133,7 +137,9 @@ the `except` suite:
- At the end of `else`, `x == 3`
- At the end of `except`, `x == 2`
```py path=single_except.py
`single_except.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -192,7 +198,9 @@ A `finally` suite is *always* executed. As such, if we reach the `reveal_type` c
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
type of `x` at the end of the example is therefore `Literal[2]`:
```py path=redef_in_finally.py
`redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -217,7 +225,9 @@ at this point than there were when we were inside the `finally` block.
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
still a TODO item for us.)
```py path=no_redef_in_finally.py
`no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -249,7 +259,9 @@ suites:
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
`except` suite ran to completion
```py path=redef_in_finally.py
`redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -286,7 +298,9 @@ itself. (In some control-flow possibilities, some exceptions were merely *suspen
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
suite.)
```py path=no_redef_in_finally.py
`no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -317,7 +331,9 @@ reveal_type(x) # revealed: str | bool
An example with multiple `except` branches and a `finally` branch:
```py path=multiple_except_branches.py
`multiple_except_branches.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -364,7 +380,9 @@ If the exception handler has an `else` branch, we must also take into account th
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
an exception raised *there*.
```py path=single_except_branch.py
`single_except_branch.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -407,7 +425,9 @@ reveal_type(x) # revealed: bool | float
The same again, this time with multiple `except` branches:
```py path=multiple_except_branches.py
`multiple_except_branches.py`:
```py
def could_raise_returns_str() -> str:
return "foo"

View File

@@ -54,7 +54,9 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"]
## Evaluates to builtin
```py path=a.py
`a.py`:
```py
redefined_builtin_bool: type[bool] = bool
def my_bool(x) -> bool:

View File

@@ -28,6 +28,8 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2]
The test inside an if expression should not affect code outside of the expression.
```py
from typing import Literal
def _(flag: bool):
x: Literal[42, "hello"] = 42 if flag else "hello"

View File

@@ -51,7 +51,7 @@ In type stubs, classes can reference themselves in their base class definitions.
This should hold true even with generics at play.
```py path=a.pyi
```pyi
class Seq[T]: ...
# TODO not error on the subscripting

View File

@@ -9,7 +9,9 @@ E = D
reveal_type(E) # revealed: Literal[C]
```
```py path=b.py
`b.py`:
```py
class C: ...
```
@@ -22,7 +24,9 @@ D = b.C
reveal_type(D) # revealed: Literal[C]
```
```py path=b.py
`b.py`:
```py
class C: ...
```
@@ -34,10 +38,14 @@ import a.b
reveal_type(a.b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```
```py path=a/b.py
`a/b.py`:
```py
class C: ...
```
@@ -49,13 +57,19 @@ import a.b.c
reveal_type(a.b.c.C) # revealed: Literal[C]
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```
```py path=a/b/__init__.py
`a/b/__init__.py`:
```py
```
```py path=a/b/c.py
`a/b/c.py`:
```py
class C: ...
```
@@ -67,10 +81,14 @@ import a.b as b
reveal_type(b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```
```py path=a/b.py
`a/b.py`:
```py
class C: ...
```
@@ -82,13 +100,19 @@ import a.b.c as c
reveal_type(c.C) # revealed: Literal[C]
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```
```py path=a/b/__init__.py
`a/b/__init__.py`:
```py
```
```py path=a/b/c.py
`a/b/c.py`:
```py
class C: ...
```
@@ -102,5 +126,7 @@ import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```

View File

@@ -29,13 +29,17 @@ builtins from the "actual" vendored typeshed:
typeshed = "/typeshed"
```
```pyi path=/typeshed/stdlib/builtins.pyi
`/typeshed/stdlib/builtins.pyi`:
```pyi
class Custom: ...
custom_builtin: Custom
```
```pyi path=/typeshed/stdlib/typing_extensions.pyi
`/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ...
```
@@ -56,12 +60,16 @@ that point:
typeshed = "/typeshed"
```
```pyi path=/typeshed/stdlib/builtins.pyi
`/typeshed/stdlib/builtins.pyi`:
```pyi
foo = bar
bar = 1
```
```pyi path=/typeshed/stdlib/typing_extensions.pyi
`/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ...
```

View File

@@ -2,7 +2,9 @@
## Maybe unbound
```py path=maybe_unbound.py
`maybe_unbound.py`:
```py
def coinflip() -> bool:
return True
@@ -29,7 +31,9 @@ reveal_type(y) # revealed: Unknown | Literal[3]
## Maybe unbound annotated
```py path=maybe_unbound_annotated.py
`maybe_unbound_annotated.py`:
```py
def coinflip() -> bool:
return True
@@ -60,7 +64,9 @@ reveal_type(y) # revealed: int
Importing a possibly undeclared name still gives us its declared type:
```py path=maybe_undeclared.py
`maybe_undeclared.py`:
```py
def coinflip() -> bool:
return True
@@ -76,11 +82,15 @@ reveal_type(x) # revealed: int
## Reimport
```py path=c.py
`c.py`:
```py
def f(): ...
```
```py path=b.py
`b.py`:
```py
def coinflip() -> bool:
return True
@@ -102,11 +112,15 @@ reveal_type(f) # revealed: Literal[f, f]
When we have a declared type in one path and only an inferred-from-definition type in the other, we
should still be able to unify those:
```py path=c.pyi
`c.pyi`:
```pyi
x: int
```
```py path=b.py
`b.py`:
```py
def coinflip() -> bool:
return True

View File

@@ -8,11 +8,15 @@ import a.b
reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
`a/__init__.py`:
```py
b: int = 42
```
```py path=a/b.py
`a/b.py`:
```py
```
## Via from/import
@@ -23,11 +27,15 @@ from a import b
reveal_type(b) # revealed: int
```
```py path=a/__init__.py
`a/__init__.py`:
```py
b: int = 42
```
```py path=a/b.py
`a/b.py`:
```py
```
## Via both
@@ -40,11 +48,15 @@ reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
`a/__init__.py`:
```py
b: int = 42
```
```py path=a/b.py
`a/b.py`:
```py
```
## Via both (backwards)
@@ -65,11 +77,15 @@ reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
`a/__init__.py`:
```py
b: int = 42
```
```py path=a/b.py
`a/b.py`:
```py
```
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement

View File

@@ -18,7 +18,9 @@ reveal_type(baz) # revealed: Unknown
## Unresolved import from resolved module
```py path=a.py
`a.py`:
```py
```
```py
@@ -29,7 +31,9 @@ reveal_type(thing) # revealed: Unknown
## Resolved import of symbol from unresolved import
```py path=a.py
`a.py`:
```py
import foo as foo # error: "Cannot resolve import `foo`"
reveal_type(foo) # revealed: Unknown
@@ -46,7 +50,9 @@ reveal_type(foo) # revealed: Unknown
## No implicit shadowing
```py path=b.py
`b.py`:
```py
x: int
```
@@ -58,7 +64,9 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
## Import cycle
```py path=a.py
`a.py`:
```py
class A: ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]]
@@ -69,7 +77,9 @@ class C(b.B): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
```
```py path=b.py
`b.py`:
```py
from a import A
class B(A): ...

View File

@@ -23,9 +23,13 @@ reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.c) # revealed: int
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```
```py path=a/b.py
`a/b.py`:
```py
c: int = 1
```

View File

@@ -2,10 +2,14 @@
## Non-existent
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/bar.py
`package/bar.py`:
```py
from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
@@ -13,14 +17,20 @@ reveal_type(X) # revealed: Unknown
## Simple
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/foo.py
`package/foo.py`:
```py
X: int = 42
```
```py path=package/bar.py
`package/bar.py`:
```py
from .foo import X
reveal_type(X) # revealed: int
@@ -28,14 +38,20 @@ reveal_type(X) # revealed: int
## Dotted
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/foo/bar/baz.py
`package/foo/bar/baz.py`:
```py
X: int = 42
```
```py path=package/bar.py
`package/bar.py`:
```py
from .foo.bar.baz import X
reveal_type(X) # revealed: int
@@ -43,11 +59,15 @@ reveal_type(X) # revealed: int
## Bare to package
```py path=package/__init__.py
`package/__init__.py`:
```py
X: int = 42
```
```py path=package/bar.py
`package/bar.py`:
```py
from . import X
reveal_type(X) # revealed: int
@@ -55,7 +75,9 @@ reveal_type(X) # revealed: int
## Non-existent + bare to package
```py path=package/bar.py
`package/bar.py`:
```py
from . import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
@@ -63,19 +85,25 @@ reveal_type(X) # revealed: Unknown
## Dunder init
```py path=package/__init__.py
`package/__init__.py`:
```py
from .foo import X
reveal_type(X) # revealed: int
```
```py path=package/foo.py
`package/foo.py`:
```py
X: int = 42
```
## Non-existent + dunder init
```py path=package/__init__.py
`package/__init__.py`:
```py
from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
@@ -83,14 +111,20 @@ reveal_type(X) # revealed: Unknown
## Long relative import
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/foo.py
`package/foo.py`:
```py
X: int = 42
```
```py path=package/subpackage/subsubpackage/bar.py
`package/subpackage/subsubpackage/bar.py`:
```py
from ...foo import X
reveal_type(X) # revealed: int
@@ -98,14 +132,20 @@ reveal_type(X) # revealed: int
## Unbound symbol
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/foo.py
`package/foo.py`:
```py
x # error: [unresolved-reference]
```
```py path=package/bar.py
`package/bar.py`:
```py
from .foo import x # error: [unresolved-import]
reveal_type(x) # revealed: Unknown
@@ -113,14 +153,20 @@ reveal_type(x) # revealed: Unknown
## Bare to module
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/foo.py
`package/foo.py`:
```py
X: int = 42
```
```py path=package/bar.py
`package/bar.py`:
```py
from . import foo
reveal_type(foo.X) # revealed: int
@@ -131,10 +177,14 @@ reveal_type(foo.X) # revealed: int
This test verifies that we emit an error when we try to import a symbol that is neither a submodule
nor an attribute of `package`.
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/bar.py
`package/bar.py`:
```py
from . import foo # error: [unresolved-import]
reveal_type(foo) # revealed: Unknown
@@ -148,14 +198,20 @@ submodule when that submodule name appears in the `imported_modules` set. That m
that are imported via `from...import` are not visible to our type inference if you also access that
submodule via the attribute on its parent package.
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/foo.py
`package/foo.py`:
```py
X: int = 42
```
```py path=package/bar.py
`package/bar.py`:
```py
from . import foo
import package

View File

@@ -9,7 +9,9 @@ y = x
reveal_type(y) # revealed: int
```
```py path=b.pyi
`b.pyi`:
```pyi
x: int
```
@@ -22,6 +24,8 @@ y = x
reveal_type(y) # revealed: int
```
```py path=b.py
`b.py`:
```py
x: int = 1
```

View File

@@ -32,10 +32,14 @@ reveal_type(a.b.C) # revealed: Literal[C]
import a.b
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```
```py path=a/b.py
`a/b.py`:
```py
class C: ...
```
@@ -55,14 +59,20 @@ reveal_type(a.b) # revealed: <module 'a.b'>
reveal_type(a.b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```
```py path=a/b.py
`a/b.py`:
```py
class C: ...
```
```py path=q.py
`q.py`:
```py
import a as a
import a.b as b
```
@@ -83,18 +93,26 @@ reveal_type(sub.b) # revealed: <module 'sub.b'>
reveal_type(attr.b) # revealed: <module 'attr.b'>
```
```py path=sub/__init__.py
`sub/__init__.py`:
```py
b = 1
```
```py path=sub/b.py
`sub/b.py`:
```py
```
```py path=attr/__init__.py
`attr/__init__.py`:
```py
from . import b as _
b = 1
```
```py path=attr/b.py
`attr/b.py`:
```py
```

View File

@@ -808,6 +808,7 @@ Dynamic types do not cancel each other out. Intersecting an unknown set of value
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
```py
from typing import Any
from knot_extensions import Intersection, Not, Unknown
def any(
@@ -830,6 +831,7 @@ def unknown(
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
```py
from typing import Any
from knot_extensions import Intersection, Not, Unknown
def mixed(

View File

@@ -31,7 +31,9 @@ reveal_type(TC) # revealed: Literal[True]
Make sure we only use our special handling for `typing.TYPE_CHECKING` and not for other constants
with the same name:
```py path=constants.py
`constants.py`:
```py
TYPE_CHECKING: bool = False
```

View File

@@ -13,6 +13,8 @@ python-version = "3.10"
Here, we simply make sure that we pick up the global configuration from the root section:
```py
import sys
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
```
@@ -25,6 +27,8 @@ reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
The same should work for arbitrarily nested sections:
```py
import sys
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
```
@@ -38,6 +42,8 @@ python-version = "3.11"
```
```py
import sys
reveal_type(sys.version_info[:2] == (3, 11)) # revealed: Literal[True]
```
@@ -46,6 +52,8 @@ reveal_type(sys.version_info[:2] == (3, 11)) # revealed: Literal[True]
There is no global state. This section should again use the root configuration:
```py
import sys
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
```
@@ -63,5 +71,7 @@ python-version = "3.12"
### Grandchild
```py
import sys
reveal_type(sys.version_info[:2] == (3, 12)) # revealed: Literal[True]
```

View File

@@ -19,13 +19,17 @@ typeshed = "/typeshed"
We can then place custom stub files in `/typeshed/stdlib`, for example:
```pyi path=/typeshed/stdlib/builtins.pyi
`/typeshed/stdlib/builtins.pyi`:
```pyi
class BuiltinClass: ...
builtin_symbol: BuiltinClass
```
```pyi path=/typeshed/stdlib/sys/__init__.pyi
`/typeshed/stdlib/sys/__init__.pyi`:
```pyi
version = "my custom Python"
```
@@ -54,15 +58,21 @@ python-version = "3.10"
typeshed = "/typeshed"
```
```pyi path=/typeshed/stdlib/old_module.pyi
`/typeshed/stdlib/old_module.pyi`:
```pyi
class OldClass: ...
```
```pyi path=/typeshed/stdlib/new_module.pyi
`/typeshed/stdlib/new_module.pyi`:
```pyi
class NewClass: ...
```
```text path=/typeshed/stdlib/VERSIONS
`/typeshed/stdlib/VERSIONS`:
```text
old_module: 3.0-
new_module: 3.11-
```
@@ -86,7 +96,9 @@ simple untyped definition is enough to make `reveal_type` work in tests:
typeshed = "/typeshed"
```
```pyi path=/typeshed/stdlib/typing_extensions.pyi
`/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ...
```

View File

@@ -205,7 +205,7 @@ reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
```py path=a.pyi
```pyi
class A(B): ... # error: [cyclic-class-definition]
class B(C): ... # error: [cyclic-class-definition]
class C(A): ... # error: [cyclic-class-definition]

View File

@@ -347,7 +347,7 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
These are invalid, but we need to be able to handle them gracefully without panicking.
```py path=a.pyi
```pyi
class Foo(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo) # revealed: Literal[Foo]
@@ -365,7 +365,7 @@ reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[objec
These are similarly unlikely, but we still shouldn't crash:
```py path=a.pyi
```pyi
class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo): ... # error: [cyclic-class-definition]
@@ -377,7 +377,7 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
## Classes with cycles in their MROs, and multiple inheritance
```py path=a.pyi
```pyi
class Spam: ...
class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition]
@@ -390,7 +390,7 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
## Classes with cycles in their MRO, and a sub-graph
```py path=a.pyi
```pyi
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]

View File

@@ -57,6 +57,8 @@ def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
## Multiple predicates
```py
from typing import Literal
def _(flag1: bool, flag2: bool):
class A: ...
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
@@ -67,6 +69,8 @@ def _(flag1: bool, flag2: bool):
## Mix of `and` and `or`
```py
from typing import Literal
def _(flag1: bool, flag2: bool):
class A: ...
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1

View File

@@ -3,6 +3,8 @@
## Value Literals
```py
from typing import Literal
def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]:
return 0
@@ -123,6 +125,8 @@ always returns a fixed value.
These types can always be fully narrowed in boolean contexts, as shown below:
```py
from typing import Literal
class T:
def __bool__(self) -> Literal[True]:
return True
@@ -149,6 +153,8 @@ else:
## Narrowing Complex Intersection and Union
```py
from typing import Literal
class A: ...
class B: ...
@@ -181,6 +187,8 @@ if isinstance(x, str) and not isinstance(x, B):
## Narrowing Multiple Variables
```py
from typing import Literal
def f(x: Literal[0, 1], y: Literal["", "hello"]):
if x and y and not x and not y:
reveal_type(x) # revealed: Never
@@ -222,6 +230,8 @@ reveal_type(y) # revealed: A
## Truthiness of classes
```py
from typing import Literal
class MetaAmbiguous(type):
def __bool__(self) -> bool: ...

View File

@@ -2,12 +2,16 @@
Regression test for [this issue](https://github.com/astral-sh/ruff/issues/14334).
```py path=base.py
`base.py`:
```py
# error: [invalid-base]
class Base(2): ...
```
```py path=a.py
`a.py`:
```py
# No error here
from base import Base
```

View File

@@ -29,7 +29,9 @@ def foo():
However, three attributes on `types.ModuleType` are not present as implicit module globals; these
are excluded:
```py path=unbound_dunders.py
`unbound_dunders.py`:
```py
# error: [unresolved-reference]
# revealed: Unknown
reveal_type(__getattr__)
@@ -70,7 +72,9 @@ Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType`
dynamic imports; but we ignore that for module-literal types where we know exactly which module
we're dealing with:
```py path=__getattr__.py
`__getattr__.py`:
```py
import typing
# error: [unresolved-attribute]
@@ -83,13 +87,17 @@ It's impossible to override the `__dict__` attribute of `types.ModuleType` insta
module; we should prioritise the attribute in the `types.ModuleType` stub over a variable named
`__dict__` in the module's global namespace:
```py path=foo.py
`foo.py`:
```py
__dict__ = "foo"
reveal_type(__dict__) # revealed: Literal["foo"]
```
```py path=bar.py
`bar.py`:
```py
import foo
from foo import __dict__ as foo_dict

View File

@@ -5,14 +5,18 @@
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function.
No diagnostics should be generated.
```py path=a.py
`a.py`:
```py
def f(x: str):
x: int = int(x)
```
## Implicit error
```py path=a.py
`a.py`:
```py
def f(): ...
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
@@ -20,7 +24,9 @@ f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explici
## Explicit shadowing
```py path=a.py
`a.py`:
```py
def f(): ...
f: int = 1

View File

@@ -7,7 +7,9 @@ branches whose conditions we can statically determine to be always true or alway
useful for `sys.version_info` branches, which can make new features available based on the Python
version:
```py path=module1.py
`module1.py`:
```py
import sys
if sys.version_info >= (3, 9):
@@ -17,7 +19,9 @@ if sys.version_info >= (3, 9):
If we can statically determine that the condition is always true, then we can also understand that
`SomeFeature` is always bound, without raising any errors:
```py path=test1.py
`test1.py`:
```py
from module1 import SomeFeature
# SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
@@ -27,11 +31,15 @@ reveal_type(SomeFeature) # revealed: str
Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used
for conditional imports:
```py path=module2.py
`module2.py`:
```py
class SomeType: ...
```
```py path=test2.py
`test2.py`:
```py
import typing
if typing.TYPE_CHECKING:
@@ -167,7 +175,11 @@ statically known conditions, but here, we show that the results are truly based
not some special handling of specific conditions in semantic index building. We use two modules to
demonstrate this, since semantic index building is inherently single-module:
```py path=module.py
`module.py`:
```py
from typing import Literal
class AlwaysTrue:
def __bool__(self) -> Literal[True]:
return True
@@ -1424,7 +1436,9 @@ def f():
#### Always false, unbound
```py path=module.py
`module.py`:
```py
if False:
symbol = 1
```
@@ -1436,7 +1450,9 @@ from module import symbol
#### Always true, bound
```py path=module.py
`module.py`:
```py
if True:
symbol = 1
```
@@ -1448,7 +1464,9 @@ from module import symbol
#### Ambiguous, possibly unbound
```py path=module.py
`module.py`:
```py
def flag() -> bool:
return True
@@ -1463,7 +1481,9 @@ from module import symbol
#### Always false, undeclared
```py path=module.py
`module.py`:
```py
if False:
symbol: int
```
@@ -1477,7 +1497,9 @@ reveal_type(symbol) # revealed: Unknown
#### Always true, declared
```py path=module.py
`module.py`:
```py
if True:
symbol: int
```

View File

@@ -5,7 +5,7 @@
In type stubs, classes can reference themselves in their base class definitions. For example, in
`typeshed`, we have `class str(Sequence[str]): ...`.
```py path=a.pyi
```pyi
class Foo[T]: ...
# TODO: actually is subscriptable

View File

@@ -5,7 +5,7 @@
The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a
stub file only, regardless of the type of the parameter.
```py path=test.pyi
```pyi
def f(x: int = ...) -> None:
reveal_type(x) # revealed: int
@@ -18,7 +18,7 @@ def f2(x: str = ...) -> None:
The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type,
in a stub file only.
```py path=test.pyi
```pyi
y: bytes = ...
reveal_type(y) # revealed: bytes
x = ...
@@ -35,7 +35,7 @@ reveal_type(Foo.y) # revealed: int
No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an
assignment statement:
```py path=test.pyi
```pyi
x, y = ...
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: Unknown
@@ -46,7 +46,7 @@ reveal_type(y) # revealed: Unknown
Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and
results in a diagnostic:
```py path=test.pyi
```pyi
# error: [not-iterable] "Object of type `ellipsis` is not iterable"
for a, b in ...:
reveal_type(a) # revealed: Unknown
@@ -72,7 +72,7 @@ reveal_type(b) # revealed: ellipsis
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
```py path=test.pyi
```pyi
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
def f(x: int = Ellipsis) -> None: ...
```

View File

@@ -97,7 +97,7 @@ reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
`typing.Tuple` can be used interchangeably with `tuple`:
```py
from typing import Tuple
from typing import Any, Tuple
class A: ...

View File

@@ -77,7 +77,8 @@ def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ...
```py
# error: [invalid-syntax]
# error: [unused-ignore-comment]
def test( # knot: ignore
def test($): # knot: ignore
pass
```
<!-- blacken-docs:on -->

View File

@@ -37,7 +37,9 @@ child expression now suppresses errors in the outer expression.
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
`"test"` and adding `"other"` to the result of the cast.
```py path=nested.py
`nested.py`:
```py
# fmt: off
from typing import cast

View File

@@ -86,14 +86,20 @@ reveal_type(bar >= (3, 9)) # revealed: Literal[True]
Only comparisons with the symbol `version_info` from the `sys` module produce literal types:
```py path=package/__init__.py
`package/__init__.py`:
```py
```
```py path=package/sys.py
`package/sys.py`:
```py
version_info: tuple[int, int] = (4, 2)
```
```py path=package/script.py
`package/script.py`:
```py
from .sys import version_info
reveal_type(version_info >= (3, 9)) # revealed: bool
@@ -103,7 +109,9 @@ reveal_type(version_info >= (3, 9)) # revealed: bool
The fields of `sys.version_info` can be accessed by name:
```py path=a.py
`a.py`:
```py
import sys
reveal_type(sys.version_info.major >= 3) # revealed: Literal[True]
@@ -114,7 +122,9 @@ reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
properties on instance types:
```py path=b.py
`b.py`:
```py
import sys
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)

View File

@@ -15,6 +15,7 @@ directly.
### Negation
```py
from typing import Literal
from knot_extensions import Not, static_assert
def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
@@ -34,7 +35,7 @@ n: Not[int, str]
```py
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
from typing_extensions import Never
from typing_extensions import Literal, Never
class S: ...
class T: ...
@@ -304,6 +305,7 @@ static_assert(not is_assignable_to(int, str))
```py
from knot_extensions import is_disjoint_from, static_assert
from typing import Literal
static_assert(is_disjoint_from(None, int))
static_assert(not is_disjoint_from(Literal[2] | str, int))
@@ -326,6 +328,7 @@ static_assert(not is_fully_static(type[Any]))
```py
from knot_extensions import is_singleton, static_assert
from typing import Literal
static_assert(is_singleton(None))
static_assert(is_singleton(Literal[True]))
@@ -338,6 +341,7 @@ static_assert(not is_singleton(Literal["a"]))
```py
from knot_extensions import is_single_valued, static_assert
from typing import Literal
static_assert(is_single_valued(None))
static_assert(is_single_valued(Literal[True]))

View File

@@ -39,7 +39,9 @@ def f(c: type[A]):
reveal_type(c) # revealed: type[A]
```
```py path=a.py
`a.py`:
```py
class A: ...
```
@@ -52,23 +54,31 @@ def f(c: type[a.B]):
reveal_type(c) # revealed: type[B]
```
```py path=a.py
`a.py`:
```py
class B: ...
```
## Deeply qualified class literal from another module
```py path=a/test.py
`a/test.py`:
```py
import a.b
def f(c: type[a.b.C]):
reveal_type(c) # revealed: type[C]
```
```py path=a/__init__.py
`a/__init__.py`:
```py
```
```py path=a/b.py
`a/b.py`:
```py
class C: ...
```

View File

@@ -6,6 +6,8 @@ This file contains tests for non-fully-static `type[]` types, such as `type[Any]
## Simple
```py
from typing import Any
def f(x: type[Any], y: type[str]):
reveal_type(x) # revealed: type[Any]
# TODO: could be `<object.__repr__ type> & Any`

View File

@@ -41,7 +41,7 @@ static types can be assignable to gradual types):
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Any
from typing import Any, Literal
static_assert(is_assignable_to(Unknown, Literal[1]))
static_assert(is_assignable_to(Any, Literal[1]))
@@ -333,7 +333,7 @@ assignable to any arbitrary type.
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing_extensions import Never, Any
from typing_extensions import Never, Any, Literal
static_assert(is_assignable_to(Never, str))
static_assert(is_assignable_to(Never, Literal[1]))

View File

@@ -151,7 +151,7 @@ static_assert(is_disjoint_from(Never, object))
### `None`
```py
from typing_extensions import Literal
from typing_extensions import Literal, LiteralString
from knot_extensions import is_disjoint_from, static_assert
static_assert(is_disjoint_from(None, Literal[True]))
@@ -245,6 +245,7 @@ static_assert(not is_disjoint_from(TypeOf[f], object))
```py
from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert
from typing import Literal
static_assert(is_disjoint_from(None, AlwaysTruthy))
static_assert(not is_disjoint_from(None, AlwaysFalsy))

View File

@@ -54,6 +54,7 @@ static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
```py
from knot_extensions import Unknown, is_gradual_equivalent_to, static_assert
from typing import Any
static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown]))

View File

@@ -148,6 +148,7 @@ static_assert(is_subtype_of(tuple[int], tuple))
```py
from knot_extensions import is_subtype_of, static_assert
from typing import Literal
class A: ...
class B1(A): ...
@@ -271,6 +272,7 @@ static_assert(is_subtype_of(Never, AlwaysFalsy))
```py
from knot_extensions import AlwaysTruthy, AlwaysFalsy, is_subtype_of, static_assert
from typing import Literal
static_assert(is_subtype_of(Literal[1], AlwaysTruthy))
static_assert(is_subtype_of(Literal[0], AlwaysFalsy))
@@ -309,7 +311,7 @@ static_assert(is_subtype_of(TypeOf[1:2:3], slice))
### Special forms
```py
from typing import _SpecialForm
from typing import _SpecialForm, Literal
from knot_extensions import TypeOf, is_subtype_of, static_assert
static_assert(is_subtype_of(TypeOf[Literal], _SpecialForm))

View File

@@ -67,6 +67,8 @@ c.a = 2
## Too many arguments
```py
from typing import ClassVar
class C:
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` expects exactly one type parameter"
x: ClassVar[int, str] = 1
@@ -75,6 +77,8 @@ class C:
## Illegal `ClassVar` in type expression
```py
from typing import ClassVar
class C:
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
x: ClassVar | int
@@ -86,6 +90,8 @@ class C:
## Used outside of a class
```py
from typing import ClassVar
# TODO: this should be an error
x: ClassVar[int] = 1
```

View File

@@ -28,7 +28,9 @@ reveal_type(not b) # revealed: Literal[False]
reveal_type(not warnings) # revealed: Literal[False]
```
```py path=b.py
`b.py`:
```py
y = 1
```
@@ -123,6 +125,8 @@ classes without a `__bool__` method, with or without `__len__`, must be inferred
truthiness.
```py
from typing import Literal
class AlwaysTrue:
def __bool__(self) -> Literal[True]:
return True

View File

@@ -361,6 +361,8 @@ def _(arg: tuple[int, int, int] | tuple[int, str, bytes] | tuple[int, int, str])
### Nested
```py
from typing import Literal
def _(arg: tuple[int, tuple[str, bytes]] | tuple[tuple[int, bytes], Literal["ab"]]):
a, (b, c) = arg
reveal_type(a) # revealed: int | tuple[int, bytes]

View File

@@ -88,6 +88,8 @@ with Manager():
## Context manager with non-callable `__exit__` attribute
```py
from typing_extensions import Self
class Manager:
def __enter__(self) -> Self: ...

View File

@@ -11,6 +11,7 @@ use ruff_index::{IndexSlice, IndexVec};
use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIds;
use crate::semantic_index::attribute_assignment::AttributeAssignments;
use crate::semantic_index::builder::SemanticIndexBuilder;
use crate::semantic_index::definition::{Definition, DefinitionNodeKey};
use crate::semantic_index::expression::Expression;
@@ -21,6 +22,7 @@ use crate::semantic_index::use_def::UseDefMap;
use crate::Db;
pub mod ast_ids;
pub mod attribute_assignment;
mod builder;
pub(crate) mod constraint;
pub mod definition;
@@ -30,7 +32,7 @@ mod use_def;
pub(crate) use self::use_def::{
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
DeclarationsIterator, ScopedVisibilityConstraintId,
DeclarationsIterator,
};
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
@@ -93,6 +95,25 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<UseD
index.use_def_map(scope.file_scope_id(db))
}
/// Returns all attribute assignments for a specific class body scope.
///
/// Using [`attribute_assignments`] over [`semantic_index`] has the advantage that
/// Salsa can avoid invalidating dependent queries if this scope's instance attributes
/// are unchanged.
#[salsa::tracked]
pub(crate) fn attribute_assignments<'db>(
db: &'db dyn Db,
class_body_scope: ScopeId<'db>,
) -> Option<Arc<AttributeAssignments<'db>>> {
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
index
.attribute_assignments
.get(&class_body_scope.file_scope_id(db))
.cloned()
}
/// Returns the module global scope of `file`.
#[salsa::tracked]
pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
@@ -139,6 +160,10 @@ pub(crate) struct SemanticIndex<'db> {
/// Flags about the global scope (code usage impacting inference)
has_future_annotations: bool,
/// Maps from class body scopes to attribute assignments that were found
/// in methods of that class.
attribute_assignments: FxHashMap<FileScopeId, Arc<AttributeAssignments<'db>>>,
}
impl<'db> SemanticIndex<'db> {

View File

@@ -0,0 +1,19 @@
use crate::semantic_index::expression::Expression;
use ruff_python_ast::name::Name;
use rustc_hash::FxHashMap;
/// Describes an (annotated) attribute assignment that we discovered in a method
/// body, typically of the form `self.x: int`, `self.x: int = …` or `self.x = …`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AttributeAssignment<'db> {
/// An attribute assignment with an explicit type annotation, either
/// `self.x: <annotation>` or `self.x: <annotation> = …`.
Annotated { annotation: Expression<'db> },
/// An attribute assignment without a type annotation, e.g. `self.x = <value>`.
Unannotated { value: Expression<'db> },
}
pub(crate) type AttributeAssignments<'db> = FxHashMap<Name, Vec<AttributeAssignment<'db>>>;

View File

@@ -14,22 +14,21 @@ use crate::ast_node_ref::AstNodeRef;
use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
use crate::semantic_index::constraint::PatternConstraintKind;
use crate::semantic_index::definition::{
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId,
SymbolTableBuilder,
};
use crate::semantic_index::use_def::{
FlowSnapshot, ScopedConstraintId, ScopedVisibilityConstraintId, UseDefMapBuilder,
};
use crate::semantic_index::use_def::{FlowSnapshot, ScopedConstraintId, UseDefMapBuilder};
use crate::semantic_index::SemanticIndex;
use crate::unpack::{Unpack, UnpackValue};
use crate::visibility_constraints::VisibilityConstraint;
use crate::visibility_constraints::{ScopedVisibilityConstraintId, VisibilityConstraintsBuilder};
use crate::Db;
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
@@ -53,17 +52,24 @@ impl LoopState {
}
}
struct ScopeInfo {
file_scope_id: FileScopeId,
loop_state: LoopState,
}
pub(super) struct SemanticIndexBuilder<'db> {
// Builder state
db: &'db dyn Db,
file: File,
module: &'db ParsedModule,
scope_stack: Vec<(FileScopeId, LoopState)>,
scope_stack: Vec<ScopeInfo>,
/// The assignments we're currently visiting, with
/// the most recent visit at the end of the Vec
current_assignments: Vec<CurrentAssignment<'db>>,
/// The match case we're currently visiting.
current_match_case: Option<CurrentMatchCase<'db>>,
/// The name of the first function parameter of the innermost function that we're currently visiting.
current_first_parameter_name: Option<&'db str>,
/// Flow states at each `break` in the current loop.
loop_break_states: Vec<FlowSnapshot>,
@@ -84,6 +90,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
imported_modules: FxHashSet<ModuleName>,
attribute_assignments: FxHashMap<FileScopeId, AttributeAssignments<'db>>,
}
impl<'db> SemanticIndexBuilder<'db> {
@@ -95,6 +102,7 @@ impl<'db> SemanticIndexBuilder<'db> {
scope_stack: Vec::new(),
current_assignments: vec![],
current_match_case: None,
current_first_parameter_name: None,
loop_break_states: vec![],
try_node_context_stack_manager: TryNodeContextStackManager::default(),
@@ -112,6 +120,8 @@ impl<'db> SemanticIndexBuilder<'db> {
expressions_by_node: FxHashMap::default(),
imported_modules: FxHashSet::default(),
attribute_assignments: FxHashMap::default(),
};
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
@@ -123,7 +133,7 @@ impl<'db> SemanticIndexBuilder<'db> {
*self
.scope_stack
.last()
.map(|(scope, _)| scope)
.map(|ScopeInfo { file_scope_id, .. }| file_scope_id)
.expect("Always to have a root scope")
}
@@ -131,14 +141,32 @@ impl<'db> SemanticIndexBuilder<'db> {
self.scope_stack
.last()
.expect("Always to have a root scope")
.1
.loop_state
}
/// Returns the scope ID of the surrounding class body scope if the current scope
/// is a method inside a class body. Returns `None` otherwise, e.g. if the current
/// scope is a function body outside of a class, or if the current scope is not a
/// function body.
fn is_method_of_class(&self) -> Option<FileScopeId> {
let mut scopes_rev = self.scope_stack.iter().rev();
let current = scopes_rev.next()?;
let parent = scopes_rev.next()?;
match (
self.scopes[current.file_scope_id].kind(),
self.scopes[parent.file_scope_id].kind(),
) {
(ScopeKind::Function, ScopeKind::Class) => Some(parent.file_scope_id),
_ => None,
}
}
fn set_inside_loop(&mut self, state: LoopState) {
self.scope_stack
.last_mut()
.expect("Always to have a root scope")
.1 = state;
.loop_state = state;
}
fn push_scope(&mut self, node: NodeWithScopeRef) {
@@ -171,16 +199,20 @@ impl<'db> SemanticIndexBuilder<'db> {
debug_assert_eq!(ast_id_scope, file_scope_id);
self.scope_stack.push((file_scope_id, LoopState::NotInLoop));
self.scope_stack.push(ScopeInfo {
file_scope_id,
loop_state: LoopState::NotInLoop,
});
}
fn pop_scope(&mut self) -> FileScopeId {
let (id, _) = self.scope_stack.pop().expect("Root scope to be present");
let ScopeInfo { file_scope_id, .. } =
self.scope_stack.pop().expect("Root scope to be present");
let children_end = self.scopes.next_index();
let scope = &mut self.scopes[id];
let scope = &mut self.scopes[file_scope_id];
scope.descendents = scope.descendents.start..children_end;
self.try_node_context_stack_manager.exit_scope();
id
file_scope_id
}
fn current_symbol_table(&mut self) -> &mut SymbolTableBuilder {
@@ -198,6 +230,11 @@ impl<'db> SemanticIndexBuilder<'db> {
&self.use_def_maps[scope_id]
}
fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder<'db> {
let scope_id = self.current_scope();
&mut self.use_def_maps[scope_id].visibility_constraints
}
fn current_ast_ids(&mut self) -> &mut AstIdsBuilder {
let scope_id = self.current_scope();
&mut self.ast_ids[scope_id]
@@ -333,21 +370,11 @@ impl<'db> SemanticIndexBuilder<'db> {
id
}
/// Adds a new visibility constraint, but does not record it. Returns the constraint ID
/// for later recording using [`SemanticIndexBuilder::record_visibility_constraint_id`].
fn add_visibility_constraint(
&mut self,
constraint: VisibilityConstraint<'db>,
) -> ScopedVisibilityConstraintId {
self.current_use_def_map_mut()
.add_visibility_constraint(constraint)
}
/// Records a previously added visibility constraint by applying it to all live bindings
/// and declarations.
fn record_visibility_constraint_id(&mut self, constraint: ScopedVisibilityConstraintId) {
self.current_use_def_map_mut()
.record_visibility_constraint_id(constraint);
.record_visibility_constraint(constraint);
}
/// Negates the given visibility constraint and then adds it to all live bindings and declarations.
@@ -355,8 +382,11 @@ impl<'db> SemanticIndexBuilder<'db> {
&mut self,
constraint: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
self.current_use_def_map_mut()
.record_visibility_constraint(VisibilityConstraint::VisibleIfNot(constraint))
let id = self
.current_visibility_constraints_mut()
.add_not_constraint(constraint);
self.record_visibility_constraint_id(id);
id
}
/// Records a visibility constraint by applying it to all live bindings and declarations.
@@ -364,8 +394,11 @@ impl<'db> SemanticIndexBuilder<'db> {
&mut self,
constraint: Constraint<'db>,
) -> ScopedVisibilityConstraintId {
self.current_use_def_map_mut()
.record_visibility_constraint(VisibilityConstraint::VisibleIf(constraint))
let id = self
.current_visibility_constraints_mut()
.add_atom(constraint, 0);
self.record_visibility_constraint_id(id);
id
}
/// Records that all remaining statements in the current block are unreachable, and therefore
@@ -374,10 +407,10 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_use_def_map_mut().mark_unreachable();
}
/// Records a [`VisibilityConstraint::Ambiguous`] constraint.
fn record_ambiguous_visibility(&mut self) -> ScopedVisibilityConstraintId {
/// Records a visibility constraint that always evaluates to "ambiguous".
fn record_ambiguous_visibility(&mut self) {
self.current_use_def_map_mut()
.record_visibility_constraint(VisibilityConstraint::Ambiguous)
.record_visibility_constraint(ScopedVisibilityConstraintId::AMBIGUOUS);
}
/// Simplifies (resets) visibility constraints on all live bindings and declarations that did
@@ -404,6 +437,32 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_assignments.last_mut()
}
/// Records the fact that we saw an attribute assignment of the form
/// `object.attr: <annotation>( = …)` or `object.attr = <value>`.
fn register_attribute_assignment(
&mut self,
object: &ast::Expr,
attr: &'db ast::Identifier,
attribute_assignment: AttributeAssignment<'db>,
) {
if let Some(class_body_scope) = self.is_method_of_class() {
// We only care about attribute assignments to the first parameter of a method,
// i.e. typically `self` or `cls`.
let accessed_object_refers_to_first_parameter =
object.as_name_expr().map(|name| name.id.as_str())
== self.current_first_parameter_name;
if accessed_object_refers_to_first_parameter {
self.attribute_assignments
.entry(class_body_scope)
.or_default()
.entry(attr.id().clone())
.or_default()
.push(attribute_assignment);
}
}
}
fn add_pattern_constraint(
&mut self,
subject: Expression<'db>,
@@ -457,6 +516,20 @@ impl<'db> SemanticIndexBuilder<'db> {
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
/// standalone (type narrowing tests, RHS of an assignment.)
fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal)
}
/// Same as [`SemanticIndexBuilder::add_standalone_expression`], but marks the expression as a
/// *type* expression, which makes sure that it will later be inferred as such.
fn add_standalone_type_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression)
}
fn add_standalone_expression_impl(
&mut self,
expression_node: &ast::Expr,
expression_kind: ExpressionKind,
) -> Expression<'db> {
let expression = Expression::new(
self.db,
self.file,
@@ -465,6 +538,7 @@ impl<'db> SemanticIndexBuilder<'db> {
unsafe {
AstNodeRef::new(self.module.clone(), expression_node)
},
expression_kind,
countme::Count::default(),
);
self.expressions_by_node
@@ -605,7 +679,7 @@ impl<'db> SemanticIndexBuilder<'db> {
}
fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) {
let symbol = self.add_symbol(parameter.parameter.name.id().clone());
let symbol = self.add_symbol(parameter.name().id().clone());
let definition = self.add_definition(symbol, parameter);
@@ -668,6 +742,11 @@ impl<'db> SemanticIndexBuilder<'db> {
use_def_maps,
imported_modules: Arc::new(self.imported_modules),
has_future_annotations: self.has_future_annotations,
attribute_assignments: self
.attribute_assignments
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect(),
}
}
}
@@ -706,7 +785,17 @@ where
builder.declare_parameters(parameters);
let mut first_parameter_name = parameters
.iter_non_variadic_params()
.next()
.map(|first_param| first_param.parameter.name.id().as_str());
std::mem::swap(
&mut builder.current_first_parameter_name,
&mut first_parameter_name,
);
builder.visit_body(body);
builder.current_first_parameter_name = first_parameter_name;
builder.pop_scope()
},
);
@@ -840,6 +929,19 @@ where
unpack: None,
first: false,
}),
ast::Expr::Attribute(ast::ExprAttribute {
value: object,
attr,
..
}) => {
self.register_attribute_assignment(
object,
attr,
AttributeAssignment::Unannotated { value },
);
None
}
_ => None,
};
@@ -858,6 +960,7 @@ where
ast::Stmt::AnnAssign(node) => {
debug_assert_eq!(&self.current_assignments, &[]);
self.visit_expr(&node.annotation);
let annotation = self.add_standalone_type_expression(&node.annotation);
if let Some(value) = &node.value {
self.visit_expr(value);
}
@@ -869,6 +972,20 @@ where
) {
self.push_assignment(node.into());
self.visit_expr(&node.target);
if let ast::Expr::Attribute(ast::ExprAttribute {
value: object,
attr,
..
}) = &*node.target
{
self.register_attribute_assignment(
object,
attr,
AttributeAssignment::Annotated { annotation },
);
}
self.pop_assignment();
} else {
self.visit_expr(&node.target);
@@ -970,6 +1087,16 @@ where
let pre_loop = self.flow_snapshot();
let constraint = self.record_expression_constraint(test);
// We need multiple copies of the visibility constraint for the while condition,
// since we need to model situations where the first evaluation of the condition
// returns True, but a later evaluation returns False.
let first_vis_constraint_id = self
.current_visibility_constraints_mut()
.add_atom(constraint, 0);
let later_vis_constraint_id = self
.current_visibility_constraints_mut()
.add_atom(constraint, 1);
// Save aside any break states from an outer loop
let saved_break_states = std::mem::take(&mut self.loop_break_states);
@@ -980,26 +1107,42 @@ where
self.visit_body(body);
self.set_inside_loop(outer_loop_state);
let vis_constraint_id = self.record_visibility_constraint(constraint);
// If the body is executed, we know that we've evaluated the condition at least
// once, and that the first evaluation was True. We might not have evaluated the
// condition more than once, so we can't assume that later evaluations were True.
// So the body's full visibility constraint is `first`.
let body_vis_constraint_id = first_vis_constraint_id;
self.record_visibility_constraint_id(body_vis_constraint_id);
// Get the break states from the body of this loop, and restore the saved outer
// ones.
let break_states =
std::mem::replace(&mut self.loop_break_states, saved_break_states);
// We may execute the `else` clause without ever executing the body, so merge in
// the pre-loop state before visiting `else`.
self.flow_merge(pre_loop.clone());
// We execute the `else` once the condition evaluates to false. This could happen
// without ever executing the body, if the condition is false the first time it's
// tested. So the starting flow state of the `else` clause is the union of:
// - the pre-loop state with a visibility constraint that the first evaluation of
// the while condition was false,
// - the post-body state (which already has a visibility constraint that the
// first evaluation was true) with a visibility constraint that a _later_
// evaluation of the while condition was false.
// To model this correctly, we need two copies of the while condition constraint,
// since the first and later evaluations might produce different results.
let post_body = self.flow_snapshot();
self.flow_restore(pre_loop.clone());
self.record_negated_visibility_constraint(first_vis_constraint_id);
self.flow_merge(post_body);
self.record_negated_constraint(constraint);
self.visit_body(orelse);
self.record_negated_visibility_constraint(vis_constraint_id);
self.record_negated_visibility_constraint(later_vis_constraint_id);
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
for break_state in break_states {
let snapshot = self.flow_snapshot();
self.flow_restore(break_state);
self.record_visibility_constraint(constraint);
self.record_visibility_constraint_id(body_vis_constraint_id);
self.flow_merge(snapshot);
}
@@ -1524,7 +1667,8 @@ where
ast::BoolOp::Or => self.add_negated_constraint(constraint),
};
let visibility_constraint = self
.add_visibility_constraint(VisibilityConstraint::VisibleIf(constraint));
.current_visibility_constraints_mut()
.add_atom(constraint, 0);
let after_expr = self.flow_snapshot();

View File

@@ -5,6 +5,16 @@ use ruff_db::files::File;
use ruff_python_ast as ast;
use salsa;
/// Whether or not this expression should be inferred as a normal expression or
/// a type expression. For example, in `self.x: <annotation> = <value>`, the
/// `<annotation>` is inferred as a type expression, while `<value>` is inferred
/// as a normal expression.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum ExpressionKind {
Normal,
TypeExpression,
}
/// An independently type-inferable expression.
///
/// Includes constraint expressions (e.g. if tests) and the RHS of an unpacking assignment.
@@ -35,6 +45,10 @@ pub(crate) struct Expression<'db> {
#[return_ref]
pub(crate) node_ref: AstNodeRef<ast::Expr>,
/// Should this expression be inferred as a normal expression or a type expression?
#[id]
pub(crate) kind: ExpressionKind,
#[no_eq]
count: countme::Count<Expression<'static>>,
}

View File

@@ -119,6 +119,7 @@ impl<'db> ScopeId<'db> {
self.node(db).scope_kind(),
ScopeKind::Annotation
| ScopeKind::Function
| ScopeKind::Lambda
| ScopeKind::TypeAlias
| ScopeKind::Comprehension
)
@@ -203,6 +204,7 @@ pub enum ScopeKind {
Annotation,
Class,
Function,
Lambda,
Comprehension,
TypeAlias,
}
@@ -443,7 +445,8 @@ impl NodeWithScopeKind {
match self {
Self::Module => ScopeKind::Module,
Self::Class(_) => ScopeKind::Class,
Self::Function(_) | Self::Lambda(_) => ScopeKind::Function,
Self::Function(_) => ScopeKind::Function,
Self::Lambda(_) => ScopeKind::Lambda,
Self::FunctionTypeParameters(_)
| Self::ClassTypeParameters(_)
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,

View File

@@ -255,16 +255,18 @@
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
//! visits a `StmtIf` node.
pub(crate) use self::symbol_state::ScopedConstraintId;
use self::symbol_state::{
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
};
pub(crate) use self::symbol_state::{ScopedConstraintId, ScopedVisibilityConstraintId};
use crate::semantic_index::ast_ids::ScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::ScopedSymbolId;
use crate::semantic_index::use_def::symbol_state::DeclarationIdWithConstraint;
use crate::visibility_constraints::{VisibilityConstraint, VisibilityConstraints};
use crate::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
};
use ruff_index::IndexVec;
use rustc_hash::FxHashMap;
@@ -285,7 +287,7 @@ pub(crate) struct UseDefMap<'db> {
/// Array of [`Constraint`] in this scope.
all_constraints: AllConstraints<'db>,
/// Array of [`VisibilityConstraint`]s in this scope.
/// Array of visibility constraints in this scope.
visibility_constraints: VisibilityConstraints<'db>,
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
@@ -487,8 +489,8 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Constraint`].
all_constraints: AllConstraints<'db>,
/// Append-only array of [`VisibilityConstraint`].
visibility_constraints: VisibilityConstraints<'db>,
/// Builder of visibility constraints.
pub(super) visibility_constraints: VisibilityConstraintsBuilder<'db>,
/// A constraint which describes the visibility of the unbound/undeclared state, i.e.
/// whether or not the start of the scope is visible. This is important for cases like
@@ -513,7 +515,7 @@ impl Default for UseDefMapBuilder<'_> {
Self {
all_definitions: IndexVec::from_iter([None]),
all_constraints: IndexVec::new(),
visibility_constraints: VisibilityConstraints::default(),
visibility_constraints: VisibilityConstraintsBuilder::default(),
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
bindings_by_use: IndexVec::new(),
definitions_by_definition: FxHashMap::default(),
@@ -561,35 +563,18 @@ impl<'db> UseDefMapBuilder<'db> {
new_constraint_id
}
pub(super) fn add_visibility_constraint(
&mut self,
constraint: VisibilityConstraint<'db>,
) -> ScopedVisibilityConstraintId {
self.visibility_constraints.add(constraint)
}
pub(super) fn record_visibility_constraint_id(
pub(super) fn record_visibility_constraint(
&mut self,
constraint: ScopedVisibilityConstraintId,
) {
for state in &mut self.symbol_states {
state.record_visibility_constraint(&mut self.visibility_constraints, constraint);
}
self.scope_start_visibility = self
.visibility_constraints
.add_and_constraint(self.scope_start_visibility, constraint);
}
pub(super) fn record_visibility_constraint(
&mut self,
constraint: VisibilityConstraint<'db>,
) -> ScopedVisibilityConstraintId {
let new_constraint_id = self.add_visibility_constraint(constraint);
self.record_visibility_constraint_id(new_constraint_id);
new_constraint_id
}
/// This method resets the visibility constraints for all symbols to a previous state
/// *if* there have been no new declarations or bindings since then. Consider the
/// following example:
@@ -742,7 +727,7 @@ impl<'db> UseDefMapBuilder<'db> {
UseDefMap {
all_definitions: self.all_definitions,
all_constraints: self.all_constraints,
visibility_constraints: self.visibility_constraints,
visibility_constraints: self.visibility_constraints.build(),
bindings_by_use: self.bindings_by_use,
public_symbols: self.symbol_states,
definitions_by_definition: self.definitions_by_definition,

View File

@@ -49,7 +49,8 @@ use ruff_index::newtype_index;
use smallvec::SmallVec;
use crate::semantic_index::use_def::bitset::{BitSet, BitSetIterator};
use crate::semantic_index::use_def::VisibilityConstraints;
use crate::semantic_index::use_def::VisibilityConstraintsBuilder;
use crate::visibility_constraints::ScopedVisibilityConstraintId;
/// A newtype-index for a definition in a particular scope.
#[newtype_index]
@@ -99,18 +100,6 @@ type ConstraintsPerBinding = SmallVec<InlineConstraintArray>;
/// Iterate over all constraints for a single binding.
type ConstraintsIterator<'a> = std::slice::Iter<'a, Constraints>;
/// A newtype-index for a visibility constraint in a particular scope.
#[newtype_index]
pub(crate) struct ScopedVisibilityConstraintId;
impl ScopedVisibilityConstraintId {
/// A special ID that is used for an "always true" / "always visible" constraint.
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
/// present at index 0.
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId::from_u32(0);
}
const INLINE_VISIBILITY_CONSTRAINTS: usize = 4;
type InlineVisibilityConstraintsArray =
[ScopedVisibilityConstraintId; INLINE_VISIBILITY_CONSTRAINTS];
@@ -164,7 +153,7 @@ impl SymbolDeclarations {
/// Add given visibility constraint to all live declarations.
pub(super) fn record_visibility_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraints,
visibility_constraints: &mut VisibilityConstraintsBuilder,
constraint: ScopedVisibilityConstraintId,
) {
for existing in &mut self.visibility_constraints {
@@ -180,7 +169,7 @@ impl SymbolDeclarations {
}
}
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraints) {
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
let a = std::mem::take(self);
self.live_declarations = a.live_declarations.clone();
self.live_declarations.union(&b.live_declarations);
@@ -270,7 +259,7 @@ impl SymbolBindings {
/// Add given visibility constraint to all live bindings.
pub(super) fn record_visibility_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraints,
visibility_constraints: &mut VisibilityConstraintsBuilder,
constraint: ScopedVisibilityConstraintId,
) {
for existing in &mut self.visibility_constraints {
@@ -287,7 +276,7 @@ impl SymbolBindings {
}
}
fn merge(&mut self, mut b: Self, visibility_constraints: &mut VisibilityConstraints) {
fn merge(&mut self, mut b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
let mut a = std::mem::take(self);
self.live_bindings = a.live_bindings.clone();
self.live_bindings.union(&b.live_bindings);
@@ -373,7 +362,7 @@ impl SymbolState {
/// Add given visibility constraint to all live bindings.
pub(super) fn record_visibility_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraints,
visibility_constraints: &mut VisibilityConstraintsBuilder,
constraint: ScopedVisibilityConstraintId,
) {
self.bindings
@@ -401,7 +390,7 @@ impl SymbolState {
pub(super) fn merge(
&mut self,
b: SymbolState,
visibility_constraints: &mut VisibilityConstraints,
visibility_constraints: &mut VisibilityConstraintsBuilder,
) {
self.bindings.merge(b.bindings, visibility_constraints);
self.declarations
@@ -584,7 +573,7 @@ mod tests {
#[test]
fn merge() {
let mut visibility_constraints = VisibilityConstraints::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
// merging the same definition with the same constraint keeps the constraint
let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
@@ -655,7 +644,7 @@ mod tests {
#[test]
fn record_declaration_merge() {
let mut visibility_constraints = VisibilityConstraints::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
@@ -669,7 +658,7 @@ mod tests {
#[test]
fn record_declaration_merge_partial_undeclared() {
let mut visibility_constraints = VisibilityConstraints::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));

View File

@@ -15,7 +15,8 @@ pub(crate) use self::diagnostic::register_lints;
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
infer_scope_types,
};
pub use self::narrow::KnownConstraintFunction;
pub(crate) use self::signatures::Signature;
@@ -23,11 +24,12 @@ pub use self::subclass_of::SubclassOfType;
use crate::module_name::ModuleName;
use crate::module_resolver::{file_to_module, resolve_module, KnownModule};
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::attribute_assignment::AttributeAssignment;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
use crate::semantic_index::{
global_scope, imported_modules, semantic_index, symbol_table, use_def_map,
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
attribute_assignments, global_scope, imported_modules, semantic_index, symbol_table,
use_def_map, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
DeclarationsIterator,
};
use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol};
@@ -53,6 +55,7 @@ mod mro;
mod narrow;
mod signatures;
mod slots;
mod statistics;
mod string_annotation;
mod subclass_of;
mod type_ordering;
@@ -4133,9 +4136,66 @@ impl<'db> Class<'db> {
}
}
// TODO: The symbol is not present in any class body, but it could be implicitly
// defined in `__init__` or other methods anywhere in the MRO.
todo_type!("implicit instance attribute").into()
SymbolAndQualifiers(Symbol::Unbound, TypeQualifiers::empty())
}
/// Tries to find declarations/bindings of an instance attribute named `name` that are only
/// "implicitly" defined in a method of the class that corresponds to `class_body_scope`.
fn implicit_instance_attribute(
db: &'db dyn Db,
class_body_scope: ScopeId<'db>,
name: &str,
inferred_type_from_class_body: Option<Type<'db>>,
) -> Symbol<'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());
if let Some(ty) = inferred_type_from_class_body {
union_of_inferred_types = union_of_inferred_types.add(ty);
}
let attribute_assignments = attribute_assignments(db, class_body_scope);
let Some(attribute_assignments) = attribute_assignments
.as_deref()
.and_then(|assignments| assignments.get(name))
else {
if inferred_type_from_class_body.is_some() {
return union_of_inferred_types.build().into();
}
return Symbol::Unbound;
};
for attribute_assignment in attribute_assignments {
match attribute_assignment {
AttributeAssignment::Annotated { annotation } => {
// We found an annotated assignment of one of the following forms (using 'self' in these
// examples, but we support arbitrary names for the first parameters of methods):
//
// self.name: <annotation>
// self.name: <annotation> = …
let annotation_ty = infer_expression_type(db, *annotation);
// TODO: check if there are conflicting declarations
return annotation_ty.into();
}
AttributeAssignment::Unannotated { value } => {
// We found an un-annotated attribute assignment of the form:
//
// self.name = <value>
let inferred_ty = infer_expression_type(db, *value);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
}
}
union_of_inferred_types.build().into()
}
/// A helper function for `instance_member` that looks up the `name` attribute only on
@@ -4157,6 +4217,8 @@ impl<'db> Class<'db> {
match symbol_from_declarations(db, declarations) {
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), 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 into properties
@@ -4170,22 +4232,26 @@ impl<'db> Class<'db> {
SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::Bound), qualifiers)
}
}
Ok(symbol @ SymbolAndQualifiers(Symbol::Unbound, 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 inferred_ty = inferred.ignore_possibly_unbound();
SymbolAndQualifiers(
widen_type_for_undeclared_public_symbol(db, inferred, symbol.is_final()),
qualifiers,
)
Self::implicit_instance_attribute(db, body_scope, name, inferred_ty).into()
}
Err((declared_ty, _conflicting_declarations)) => {
// Ignore conflicting declarations
// There are conflicting declarations for this attribute in the class body.
SymbolAndQualifiers(declared_ty.inner_type().into(), declared_ty.qualifiers())
}
}
} else {
Symbol::Unbound.into()
// 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, None).into()
}
}

View File

@@ -3,7 +3,7 @@
use std::fmt::{self, Display, Formatter, Write};
use ruff_db::display::FormatterJoinExtension;
use ruff_python_ast::str::Quote;
use ruff_python_ast::str::{Quote, TripleQuotes};
use ruff_python_literal::escape::AsciiEscape;
use crate::types::class_base::ClassBase;
@@ -98,7 +98,7 @@ impl Display for DisplayRepresentation<'_> {
let escape =
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
escape.bytes_repr().write(f)
escape.bytes_repr(TripleQuotes::No).write(f)
}
Type::SliceLiteral(slice) => {
f.write_str("slice[")?;

View File

@@ -44,7 +44,7 @@ use crate::semantic_index::definition::{
AssignmentDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey,
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind,
};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
use crate::semantic_index::SemanticIndex;
@@ -61,6 +61,7 @@ use crate::types::diagnostic::{
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
};
use crate::types::mro::MroErrorKind;
use crate::types::statistics::TypeStatistics;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
builtins_symbol, global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
@@ -192,6 +193,20 @@ pub(crate) fn infer_expression_types<'db>(
TypeInferenceBuilder::new(db, InferenceRegion::Expression(expression), index).finish()
}
// Similar to `infer_expression_types` (with the same restrictions). Directly returns the
// type of the overall expression. This is a salsa query because it accesses `node_ref`,
// which is sensitive to changes in the AST. Making it a query allows downstream queries
// to short-circuit if the result type has not changed.
#[salsa::tracked]
pub(crate) fn infer_expression_type<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> Type<'db> {
let inference = infer_expression_types(db, expression);
let expr_scope = expression.scope(db);
inference.expression_type(expression.node_ref(db).scoped_expression_id(db, expr_scope))
}
/// Infer the types for an [`Unpack`] operation.
///
/// This infers the expression type and performs structural match against the target expression
@@ -299,6 +314,14 @@ impl<'db> TypeInference<'db> {
self.diagnostics.shrink_to_fit();
self.deferred.shrink_to_fit();
}
pub(super) fn statistics(&self) -> TypeStatistics {
let mut statistics = TypeStatistics::default();
for ty in self.expressions.values() {
statistics.increment(*ty);
}
statistics
}
}
impl WithDiagnostics for TypeInference<'_> {
@@ -823,7 +846,14 @@ impl<'db> TypeInferenceBuilder<'db> {
}
fn infer_region_expression(&mut self, expression: Expression<'db>) {
self.infer_expression_impl(expression.node_ref(self.db()));
match expression.kind(self.db()) {
ExpressionKind::Normal => {
self.infer_expression_impl(expression.node_ref(self.db()));
}
ExpressionKind::TypeExpression => {
self.infer_type_expression(expression.node_ref(self.db()));
}
}
}
/// Raise a diagnostic if the given type cannot be divided by zero.
@@ -1285,7 +1315,7 @@ impl<'db> TypeInferenceBuilder<'db> {
parameter: &ast::Parameter,
definition: Definition<'db>,
) {
if let Some(annotation) = parameter.annotation.as_ref() {
if let Some(annotation) = parameter.annotation() {
let _annotated_ty = self.file_expression_type(annotation);
// TODO `tuple[annotated_ty, ...]`
let ty = KnownClass::Tuple.to_instance(self.db());
@@ -1314,7 +1344,7 @@ impl<'db> TypeInferenceBuilder<'db> {
parameter: &ast::Parameter,
definition: Definition<'db>,
) {
if let Some(annotation) = parameter.annotation.as_ref() {
if let Some(annotation) = parameter.annotation() {
let _annotated_ty = self.file_expression_type(annotation);
// TODO `dict[str, annotated_ty]`
let ty = KnownClass::Dict.to_instance(self.db());
@@ -6010,7 +6040,7 @@ mod tests {
use crate::types::check_types;
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::DbWithTestSystem;
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run};
use super::*;
@@ -6337,4 +6367,84 @@ mod tests {
);
Ok(())
}
#[test]
fn dependency_implicit_instance_attribute() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main);
// Get the second statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
let index = semantic_index(db, file_main);
index.expression(x_rhs_node.as_ref())
}
let mut db = setup_db();
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
self.attr: int | None = None
"#,
)?;
db.write_dedented(
"/src/main.py",
r#"
from mod import C
x = C().attr
"#,
)?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").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
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
self.attr: str | None = None
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// Add a comment; this should not trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
# a comment!
self.attr: str | None = None
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_not_run(
&db,
infer_expression_types,
x_rhs_expression(&db),
&events,
);
Ok(())
}
}

View File

@@ -93,10 +93,9 @@ impl<'db> Parameters<'db> {
kwarg,
range: _,
} = parameters;
let default_ty = |parameter_with_default: &ast::ParameterWithDefault| {
parameter_with_default
.default
.as_deref()
let default_ty = |param: &ast::ParameterWithDefault| {
param
.default()
.map(|default| definition_expression_type(db, definition, default))
};
let positional_only = posonlyargs.iter().map(|arg| {
@@ -243,8 +242,7 @@ impl<'db> Parameter<'db> {
Self {
name: Some(parameter.name.id.clone()),
annotated_ty: parameter
.annotation
.as_deref()
.annotation()
.map(|annotation| definition_expression_type(db, definition, annotation)),
kind,
}

View File

@@ -0,0 +1,121 @@
use crate::types::{infer_scope_types, semantic_index, Type};
use crate::Db;
use ruff_db::files::File;
use rustc_hash::FxHashMap;
/// Get type-coverage statistics for a file.
#[salsa::tracked(return_ref)]
pub fn type_statistics<'db>(db: &'db dyn Db, file: File) -> TypeStatistics<'db> {
let _span = tracing::trace_span!("type_statistics", file=?file.path(db)).entered();
tracing::debug!(
"Gathering statistics for file '{path}'",
path = file.path(db)
);
let index = semantic_index(db, file);
let mut statistics = TypeStatistics::default();
for scope_id in index.scope_ids() {
let result = infer_scope_types(db, scope_id);
statistics.extend(&result.statistics());
}
statistics
}
/// Map each type to count of expressions with that type.
#[derive(Debug, Default, Eq, PartialEq)]
pub(super) struct TypeStatistics<'db>(FxHashMap<Type<'db>, u32>);
impl<'db> TypeStatistics<'db> {
fn extend(&mut self, other: &TypeStatistics<'db>) {
for (ty, count) in &other.0 {
self.0
.entry(*ty)
.and_modify(|my_count| *my_count += count)
.or_insert(*count);
}
}
pub(super) fn increment(&mut self, ty: Type<'db>) {
self.0
.entry(ty)
.and_modify(|count| *count += 1)
.or_insert(1);
}
#[allow(unused)]
fn expression_count(&self) -> u32 {
self.0.values().sum()
}
#[allow(unused)]
fn todo_count(&self) -> u32 {
self.0
.iter()
.filter(|(key, _)| key.is_todo())
.map(|(_, count)| count)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::tests::{setup_db, TestDb};
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem;
fn get_stats<'db>(
db: &'db mut TestDb,
filename: &str,
source: &str,
) -> &'db TypeStatistics<'db> {
db.write_dedented(filename, source).unwrap();
type_statistics(db, system_path_to_file(db, filename).unwrap())
}
#[test]
fn all_static() {
let mut db = setup_db();
let stats = get_stats(&mut db, "src/foo.py", "1");
assert_eq!(stats.0, FxHashMap::from_iter([(Type::IntLiteral(1), 1)]));
}
#[test]
fn todo_and_expression_count() {
let mut db = setup_db();
let stats = get_stats(
&mut db,
"src/foo.py",
r#"
x = [x for x in [1]]
"#,
);
assert_eq!(stats.todo_count(), 4);
assert_eq!(stats.expression_count(), 6);
}
#[test]
fn sum() {
let mut db = setup_db();
let stats = get_stats(
&mut db,
"src/foo.py",
r#"
1
def f():
1
"#,
);
assert_eq!(stats.0[&Type::IntLiteral(1)], 2);
}
}

View File

@@ -7,7 +7,7 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::symbol::ScopeId;
use crate::types::{infer_expression_types, todo_type, Type, TypeCheckDiagnostics};
use crate::types::{infer_expression_type, todo_type, Type, TypeCheckDiagnostics};
use crate::unpack::UnpackValue;
use crate::Db;
@@ -42,8 +42,7 @@ impl<'db> Unpacker<'db> {
"Unpacking target must be a list or tuple expression"
);
let mut value_ty = infer_expression_types(self.db(), value.expression())
.expression_type(value.scoped_expression_id(self.db(), self.scope));
let mut value_ty = infer_expression_type(self.db(), value.expression());
if value.is_assign()
&& self.context.in_stub()

View File

@@ -3,7 +3,6 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
use crate::ast_node_ref::AstNodeRef;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
use crate::Db;
@@ -88,17 +87,6 @@ impl<'db> UnpackValue<'db> {
}
}
/// Returns the [`ScopedExpressionId`] of the underlying expression.
pub(crate) fn scoped_expression_id(
self,
db: &'db dyn Db,
scope: ScopeId<'db>,
) -> ScopedExpressionId {
self.expression()
.node_ref(db)
.scoped_expression_id(db, scope)
}
/// Returns the expression as an [`AnyNodeRef`].
pub(crate) fn as_any_node_ref(self, db: &'db dyn Db) -> AnyNodeRef<'db> {
self.expression().node_ref(db).node().into()

View File

@@ -122,7 +122,7 @@
//!
//! ### Explicit ambiguity
//!
//! In some cases, we explicitly add a `VisibilityConstraint::Ambiguous` constraint to all bindings
//! In some cases, we explicitly add an “ambiguous constraint to all bindings
//! in a certain control flow path. We do this when branching on something that we can not (or
//! intentionally do not want to) analyze statically. `for` loops are one example:
//! ```py
@@ -150,14 +150,10 @@
//!
//! [Kleene]: <https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics>
use ruff_index::IndexVec;
use ruff_index::{newtype_index, IndexVec};
use crate::semantic_index::ScopedVisibilityConstraintId;
use crate::semantic_index::{
ast_ids::HasScopedExpressionId,
constraint::{Constraint, ConstraintNode, PatternConstraintKind},
};
use crate::types::{infer_expression_types, Truthiness};
use crate::semantic_index::constraint::{Constraint, ConstraintNode, PatternConstraintKind};
use crate::types::{infer_expression_type, Truthiness};
use crate::Db;
/// The maximum depth of recursion when evaluating visibility constraints.
@@ -168,35 +164,113 @@ use crate::Db;
/// resulting from a few files with a lot of boolean expressions and `if`-statements.
const MAX_RECURSION_DEPTH: usize = 24;
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
/// is just like a boolean formula, but with `Ambiguous` as a third potential result. See the
/// module documentation for more details.)
///
/// The primitive atoms of the formula are [`Constraint`]s, which express some property of the
/// runtime state of the code that we are analyzing.
///
/// We assume that each atom has a stable value each time that the formula is evaluated. An atom
/// that resolves to `Ambiguous` might be true or false, and we can't tell which — but within that
/// evaluation, we assume that the atom has the _same_ unknown value each time it appears. That
/// allows us to perform simplifications like `A !A → true` and `A ∧ !A → false`.
///
/// That means that when you are constructing a formula, you might need to create distinct atoms
/// for a particular [`Constraint`], if your formula needs to consider how a particular runtime
/// property might be different at different points in the execution of the program.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum VisibilityConstraint<'db> {
pub(crate) struct VisibilityConstraint<'db>(VisibilityConstraintInner<'db>);
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum VisibilityConstraintInner<'db> {
AlwaysTrue,
AlwaysFalse,
Ambiguous,
VisibleIf(Constraint<'db>),
VisibleIf(Constraint<'db>, u8),
VisibleIfNot(ScopedVisibilityConstraintId),
KleeneAnd(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
KleeneOr(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
}
/// A newtype-index for a visibility constraint in a particular scope.
#[newtype_index]
pub(crate) struct ScopedVisibilityConstraintId;
impl ScopedVisibilityConstraintId {
/// A special ID that is used for an "always true" / "always visible" constraint.
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
/// present at index 0.
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId::from_u32(0);
/// A special ID that is used for an "always false" / "never visible" constraint.
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
/// present at index 1.
pub(crate) const ALWAYS_FALSE: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId::from_u32(1);
/// A special ID that is used for an ambiguous constraint.
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
/// present at index 2.
pub(crate) const AMBIGUOUS: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId::from_u32(2);
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct VisibilityConstraints<'db> {
constraints: IndexVec<ScopedVisibilityConstraintId, VisibilityConstraint<'db>>,
}
impl Default for VisibilityConstraints<'_> {
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct VisibilityConstraintsBuilder<'db> {
constraints: IndexVec<ScopedVisibilityConstraintId, VisibilityConstraint<'db>>,
}
impl Default for VisibilityConstraintsBuilder<'_> {
fn default() -> Self {
Self {
constraints: IndexVec::from_iter([VisibilityConstraint::AlwaysTrue]),
constraints: IndexVec::from_iter([
VisibilityConstraint(VisibilityConstraintInner::AlwaysTrue),
VisibilityConstraint(VisibilityConstraintInner::AlwaysFalse),
VisibilityConstraint(VisibilityConstraintInner::Ambiguous),
]),
}
}
}
impl<'db> VisibilityConstraints<'db> {
pub(crate) fn add(
impl<'db> VisibilityConstraintsBuilder<'db> {
pub(crate) fn build(self) -> VisibilityConstraints<'db> {
VisibilityConstraints {
constraints: self.constraints,
}
}
fn add(&mut self, constraint: VisibilityConstraintInner<'db>) -> ScopedVisibilityConstraintId {
self.constraints.push(VisibilityConstraint(constraint))
}
pub(crate) fn add_atom(
&mut self,
constraint: VisibilityConstraint<'db>,
constraint: Constraint<'db>,
copy: u8,
) -> ScopedVisibilityConstraintId {
self.constraints.push(constraint)
self.add(VisibilityConstraintInner::VisibleIf(constraint, copy))
}
pub(crate) fn add_not_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
if a == ScopedVisibilityConstraintId::ALWAYS_FALSE {
ScopedVisibilityConstraintId::ALWAYS_TRUE
} else if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
ScopedVisibilityConstraintId::ALWAYS_FALSE
} else if a == ScopedVisibilityConstraintId::AMBIGUOUS {
ScopedVisibilityConstraintId::AMBIGUOUS
} else {
self.add(VisibilityConstraintInner::VisibleIfNot(a))
}
}
pub(crate) fn add_or_constraint(
@@ -204,14 +278,23 @@ impl<'db> VisibilityConstraints<'db> {
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
if a == ScopedVisibilityConstraintId::ALWAYS_TRUE
|| b == ScopedVisibilityConstraintId::ALWAYS_TRUE
{
return ScopedVisibilityConstraintId::ALWAYS_TRUE;
} else if a == ScopedVisibilityConstraintId::ALWAYS_FALSE {
return b;
} else if b == ScopedVisibilityConstraintId::ALWAYS_FALSE {
return a;
}
match (&self.constraints[a], &self.constraints[b]) {
(_, VisibilityConstraint::VisibleIfNot(id)) if a == *id => {
(_, VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id))) if a == *id => {
ScopedVisibilityConstraintId::ALWAYS_TRUE
}
(VisibilityConstraint::VisibleIfNot(id), _) if *id == b => {
(VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id)), _) if *id == b => {
ScopedVisibilityConstraintId::ALWAYS_TRUE
}
_ => self.add(VisibilityConstraint::KleeneOr(a, b)),
_ => self.add(VisibilityConstraintInner::KleeneOr(a, b)),
}
}
@@ -220,15 +303,28 @@ impl<'db> VisibilityConstraints<'db> {
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
b
if a == ScopedVisibilityConstraintId::ALWAYS_FALSE
|| b == ScopedVisibilityConstraintId::ALWAYS_FALSE
{
return ScopedVisibilityConstraintId::ALWAYS_FALSE;
} else if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
return b;
} else if b == ScopedVisibilityConstraintId::ALWAYS_TRUE {
a
} else {
self.add(VisibilityConstraint::KleeneAnd(a, b))
return a;
}
match (&self.constraints[a], &self.constraints[b]) {
(_, VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id))) if a == *id => {
ScopedVisibilityConstraintId::ALWAYS_FALSE
}
(VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id)), _) if *id == b => {
ScopedVisibilityConstraintId::ALWAYS_FALSE
}
_ => self.add(VisibilityConstraintInner::KleeneAnd(a, b)),
}
}
}
impl<'db> VisibilityConstraints<'db> {
/// Analyze the statically known visibility for a given visibility constraint.
pub(crate) fn evaluate(&self, db: &'db dyn Db, id: ScopedVisibilityConstraintId) -> Truthiness {
self.evaluate_impl(db, id, MAX_RECURSION_DEPTH)
@@ -244,15 +340,18 @@ impl<'db> VisibilityConstraints<'db> {
return Truthiness::Ambiguous;
}
let visibility_constraint = &self.constraints[id];
let VisibilityConstraint(visibility_constraint) = &self.constraints[id];
match visibility_constraint {
VisibilityConstraint::AlwaysTrue => Truthiness::AlwaysTrue,
VisibilityConstraint::Ambiguous => Truthiness::Ambiguous,
VisibilityConstraint::VisibleIf(constraint) => Self::analyze_single(db, constraint),
VisibilityConstraint::VisibleIfNot(negated) => {
VisibilityConstraintInner::AlwaysTrue => Truthiness::AlwaysTrue,
VisibilityConstraintInner::AlwaysFalse => Truthiness::AlwaysFalse,
VisibilityConstraintInner::Ambiguous => Truthiness::Ambiguous,
VisibilityConstraintInner::VisibleIf(constraint, _) => {
Self::analyze_single(db, constraint)
}
VisibilityConstraintInner::VisibleIfNot(negated) => {
self.evaluate_impl(db, *negated, max_depth - 1).negate()
}
VisibilityConstraint::KleeneAnd(lhs, rhs) => {
VisibilityConstraintInner::KleeneAnd(lhs, rhs) => {
let lhs = self.evaluate_impl(db, *lhs, max_depth - 1);
if lhs == Truthiness::AlwaysFalse {
@@ -269,7 +368,7 @@ impl<'db> VisibilityConstraints<'db> {
Truthiness::Ambiguous
}
}
VisibilityConstraint::KleeneOr(lhs_id, rhs_id) => {
VisibilityConstraintInner::KleeneOr(lhs_id, rhs_id) => {
let lhs = self.evaluate_impl(db, *lhs_id, max_depth - 1);
if lhs == Truthiness::AlwaysTrue {
@@ -292,28 +391,15 @@ impl<'db> VisibilityConstraints<'db> {
fn analyze_single(db: &dyn Db, constraint: &Constraint) -> Truthiness {
match constraint.node {
ConstraintNode::Expression(test_expr) => {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
let ty = inference
.expression_type(test_expr.node_ref(db).scoped_expression_id(db, scope));
let ty = infer_expression_type(db, test_expr);
ty.bool(db).negate_if(!constraint.is_positive)
}
ConstraintNode::Pattern(inner) => match inner.kind(db) {
PatternConstraintKind::Value(value, guard) => {
let subject_expression = inner.subject(db);
let inference = infer_expression_types(db, *subject_expression);
let scope = subject_expression.scope(db);
let subject_ty = inference.expression_type(
subject_expression
.node_ref(db)
.scoped_expression_id(db, scope),
);
let inference = infer_expression_types(db, *value);
let scope = value.scope(db);
let value_ty = inference
.expression_type(value.node_ref(db).scoped_expression_id(db, scope));
let subject_ty = infer_expression_type(db, *subject_expression);
let value_ty = infer_expression_type(db, *value);
if subject_ty.is_single_valued(db) {
let truthiness =

View File

@@ -20,10 +20,10 @@ reveal_type(1) # revealed: Literal[1]
````
When running this test, the mdtest framework will write a file with these contents to the default
file path (`/src/test.py`) in its in-memory file system, run a type check on that file, and then
match the resulting diagnostics with the assertions in the test. Assertions are in the form of
Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise, it
fails.
file path (`/src/mdtest_snippet__1.py`) in its in-memory file system, run a type check on that file,
and then match the resulting diagnostics with the assertions in the test. Assertions are in the form
of Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise,
it fails.
<!---
(If you are reading this document in raw Markdown source rather than rendered Markdown, note that
@@ -129,8 +129,12 @@ assertion as the line of source code on which the matched diagnostics are emitte
## Multi-file tests
Some tests require multiple files, with imports from one file into another. Multiple fenced code
blocks represent multiple embedded files. Since files must have unique names, at most one file can
use the default name of `/src/test.py`. Other files must explicitly specify their file name:
blocks represent multiple embedded files. If there are multiple unnamed files, mdtest will name them
according to the numbered scheme `/src/mdtest_snippet__1.py`, `/src/mdtest_snippet__2.py`, etc. (If
they are `pyi` files, they will be named with a `pyi` extension instead.)
Tests should not rely on these default names. If a test must import from a file, then it should
explicitly specify the file name:
````markdown
```py
@@ -138,7 +142,9 @@ from b import C
reveal_type(C) # revealed: Literal[C]
```
```py path=b.py
`b.py`:
```py
class C: pass
```
````
@@ -149,8 +155,8 @@ is, the equivalent of a runtime entry on `sys.path`).
The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but
this is a feature we will want to add in the future.
So the above test creates two files, `/src/test.py` and `/src/b.py`, and sets the workspace root to
`/src/`, allowing `test.py` to import from `b.py` using the module name `b`.
So the above test creates two files, `/src/mdtest_snippet__1.py` and `/src/b.py`, and sets the
workspace root to `/src/`, allowing imports from `b.py` using the module name `b`.
## Multi-test suites
@@ -171,7 +177,9 @@ from b import y
x: int = y # error: [invalid-assignment]
```
```py path=b.py
`b.py`:
```py
y = "foo"
```
````
@@ -357,17 +365,17 @@ This is just an example, not a proposal that red-knot would ever actually output
precisely this format:
```output
test.py, line 1, col 1: revealed type is 'Literal[1]'
mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
```
````
We will want to build tooling to automatically capture and update these “full diagnostic output”
blocks, when tests are run in an update-output mode (probably specified by an environment variable.)
By default, an `output` block will specify diagnostic output for the file `<workspace-root>/test.py`.
An `output` block can have a `path=` option, to explicitly specify the Python file for which it
asserts diagnostic output, and a `stage=` option, to specify which stage of an incremental test it
specifies diagnostic output at. (See “incremental tests” below.)
By default, an `output` block will specify diagnostic output for the file
`<workspace-root>/mdtest_snippet__1.py`. An `output` block can be prefixed by a
<code>`&lt;path>`:</code> label as usual, to explicitly specify the Python file for which it asserts
diagnostic output.
It is an error for an `output` block to exist, if there is no `py` or `python` block in the same
test for the same file path.
@@ -385,39 +393,43 @@ fenced code blocks in the test:
## modify a file
Initial version of `test.py` and `b.py`:
Initial file contents:
```py
from b import x
reveal_type(x)
```
```py path=b.py
`b.py`:
```py
x = 1
```
Initial expected output for `test.py`:
Initial expected output for the unnamed file:
```output
/src/test.py, line 1, col 1: revealed type is 'Literal[1]'
/src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
```
Now in our first incremental stage, modify the contents of `b.py`:
```py path=b.py stage=1
`b.py`:
```py stage=1
# b.py
x = 2
```
And this is our updated expected output for `test.py` at stage 1:
And this is our updated expected output for the unnamed file at stage 1:
```output stage=1
/src/test.py, line 1, col 1: revealed type is 'Literal[2]'
/src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[2]'
```
(One reason to use full-diagnostic-output blocks in this test is that updating
inline-comment diagnostic assertions for `test.py` would require specifying new
contents for `test.py` in stage 1, which we don't want to do in this test.)
(One reason to use full-diagnostic-output blocks in this test is that updating inline-comment
diagnostic assertions for `mdtest_snippet__1.py` would require specifying new contents for
`mdtest_snippet__1.py` in stage 1, which we don't want to do in this test.)
````
It will be possible to provide any number of stages in an incremental test. If a stage re-specifies

View File

@@ -109,9 +109,9 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
);
let full_path = if embedded.path.starts_with('/') {
SystemPathBuf::from(embedded.path)
SystemPathBuf::from(embedded.path.clone())
} else {
project_root.join(embedded.path)
project_root.join(&embedded.path)
};
if let Some(ref typeshed_path) = custom_typeshed_path {
@@ -135,7 +135,7 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
Some(TestFile {
file,
backtick_offset: embedded.md_offset,
backtick_offset: embedded.backtick_offset,
})
})
.collect();

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
101287091cbd71a3305a4fc4a1a8eb5df0e3f6f7
c193cd2a36839c8e6336f350397f51ce52fedd5e

View File

@@ -27,14 +27,14 @@ _TrapType: TypeAlias = type[DecimalException]
__version__: Final[str]
__libmpdec_version__: Final[str]
ROUND_DOWN: Final[str]
ROUND_HALF_UP: Final[str]
ROUND_HALF_EVEN: Final[str]
ROUND_CEILING: Final[str]
ROUND_FLOOR: Final[str]
ROUND_UP: Final[str]
ROUND_HALF_DOWN: Final[str]
ROUND_05UP: Final[str]
ROUND_DOWN: Final = "ROUND_DOWN"
ROUND_HALF_UP: Final = "ROUND_HALF_UP"
ROUND_HALF_EVEN: Final = "ROUND_HALF_EVEN"
ROUND_CEILING: Final = "ROUND_CEILING"
ROUND_FLOOR: Final = "ROUND_FLOOR"
ROUND_UP: Final = "ROUND_UP"
ROUND_HALF_DOWN: Final = "ROUND_HALF_DOWN"
ROUND_05UP: Final = "ROUND_05UP"
HAVE_CONTEXTVAR: Final[bool]
HAVE_THREADS: Final[bool]
MAX_EMAX: Final[int]

View File

@@ -1,13 +1,13 @@
from typing import SupportsComplex, SupportsFloat, SupportsIndex
from typing import Final, SupportsComplex, SupportsFloat, SupportsIndex
from typing_extensions import TypeAlias
e: float
pi: float
inf: float
infj: complex
nan: float
nanj: complex
tau: float
e: Final[float]
pi: Final[float]
inf: Final[float]
infj: Final[complex]
nan: Final[float]
nanj: Final[complex]
tau: Final[float]
_C: TypeAlias = SupportsFloat | SupportsComplex | SupportsIndex | complex

View File

@@ -32,9 +32,9 @@ _T = TypeVar("_T")
_T_co = TypeVar("_T_co", covariant=True)
_T_io = TypeVar("_T_io", bound=IO[str] | None)
_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None)
_F = TypeVar("_F", bound=Callable[..., Any])
_G = TypeVar("_G", bound=Generator[Any, Any, Any] | AsyncGenerator[Any, Any], covariant=True)
_P = ParamSpec("_P")
_R = TypeVar("_R")
_SendT_contra = TypeVar("_SendT_contra", contravariant=True, default=None)
_ReturnT_co = TypeVar("_ReturnT_co", covariant=True, default=None)
@@ -64,9 +64,13 @@ class AbstractAsyncContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ign
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
) -> _ExitT_co: ...
class _WrappedCallable(Generic[_P, _R]):
__wrapped__: Callable[_P, _R]
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
class ContextDecorator:
def _recreate_cm(self) -> Self: ...
def __call__(self, func: _F) -> _F: ...
def __call__(self, func: Callable[_P, _R]) -> _WrappedCallable[_P, _R]: ...
class _GeneratorContextManagerBase(Generic[_G]):
# Ideally this would use ParamSpec, but that requires (*args, **kwargs), which this isn't. see #6676
@@ -93,11 +97,11 @@ class _GeneratorContextManager(
def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ...
if sys.version_info >= (3, 10):
_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]])
_AR = TypeVar("_AR", bound=Awaitable[Any])
class AsyncContextDecorator:
def _recreate_cm(self) -> Self: ...
def __call__(self, func: _AF) -> _AF: ...
def __call__(self, func: Callable[_P, _AR]) -> _WrappedCallable[_P, _AR]: ...
class _AsyncGeneratorContextManager(
_GeneratorContextManagerBase[AsyncGenerator[_T_co, _SendT_contra]],

View File

@@ -65,7 +65,7 @@ class Underflow(Inexact, Rounded, Subnormal): ...
class FloatOperation(DecimalException, TypeError): ...
class Decimal:
def __new__(cls, value: _DecimalNew = ..., context: Context | None = ...) -> Self: ...
def __new__(cls, value: _DecimalNew = "0", context: Context | None = None) -> Self: ...
@classmethod
def from_float(cls, f: float, /) -> Self: ...
def __bool__(self) -> bool: ...
@@ -163,12 +163,12 @@ class Decimal:
def __reduce__(self) -> tuple[type[Self], tuple[str]]: ...
def __copy__(self) -> Self: ...
def __deepcopy__(self, memo: Any, /) -> Self: ...
def __format__(self, specifier: str, context: Context | None = ..., /) -> str: ...
def __format__(self, specifier: str, context: Context | None = None, /) -> str: ...
class Context:
# TODO: Context doesn't allow you to delete *any* attributes from instances of the class at runtime,
# even settable attributes like `prec` and `rounding`,
# but that's inexpressable in the stub.
# but that's inexpressible in the stub.
# Type checkers either ignore it or misinterpret it
# if you add a `def __delattr__(self, name: str, /) -> NoReturn` method to the stub
prec: int
@@ -181,14 +181,14 @@ class Context:
flags: dict[_TrapType, bool]
def __init__(
self,
prec: int | None = ...,
rounding: str | None = ...,
Emin: int | None = ...,
Emax: int | None = ...,
capitals: int | None = ...,
clamp: int | None = ...,
flags: None | dict[_TrapType, bool] | Container[_TrapType] = ...,
traps: None | dict[_TrapType, bool] | Container[_TrapType] = ...,
prec: int | None = None,
rounding: str | None = None,
Emin: int | None = None,
Emax: int | None = None,
capitals: int | None = None,
clamp: int | None = None,
flags: dict[_TrapType, bool] | Container[_TrapType] | None = None,
traps: dict[_TrapType, bool] | Container[_TrapType] | None = None,
) -> None: ...
def __reduce__(self) -> tuple[type[Self], tuple[Any, ...]]: ...
def clear_flags(self) -> None: ...

View File

@@ -16,7 +16,7 @@ if sys.version_info >= (3, 11):
Anchor: TypeAlias = Package
def package_to_anchor(
func: Callable[[Anchor | None], Traversable]
func: Callable[[Anchor | None], Traversable],
) -> Callable[[Anchor | None, Anchor | None], Traversable]: ...
@overload
def files(anchor: Anchor | None = None) -> Traversable: ...

View File

@@ -370,7 +370,7 @@ if sys.version_info >= (3, 12):
AGEN_CLOSED: Final = "AGEN_CLOSED"
def getasyncgenstate(
agen: AsyncGenerator[Any, Any]
agen: AsyncGenerator[Any, Any],
) -> Literal["AGEN_CREATED", "AGEN_RUNNING", "AGEN_SUSPENDED", "AGEN_CLOSED"]: ...
def getasyncgenlocals(agen: AsyncGeneratorType[Any, Any]) -> dict[str, Any]: ...
@@ -590,7 +590,7 @@ GEN_SUSPENDED: Final = "GEN_SUSPENDED"
GEN_CLOSED: Final = "GEN_CLOSED"
def getgeneratorstate(
generator: Generator[Any, Any, Any]
generator: Generator[Any, Any, Any],
) -> Literal["GEN_CREATED", "GEN_RUNNING", "GEN_SUSPENDED", "GEN_CLOSED"]: ...
CORO_CREATED: Final = "CORO_CREATED"
@@ -599,7 +599,7 @@ CORO_SUSPENDED: Final = "CORO_SUSPENDED"
CORO_CLOSED: Final = "CORO_CLOSED"
def getcoroutinestate(
coroutine: Coroutine[Any, Any, Any]
coroutine: Coroutine[Any, Any, Any],
) -> Literal["CORO_CREATED", "CORO_RUNNING", "CORO_SUSPENDED", "CORO_CLOSED"]: ...
def getgeneratorlocals(generator: Generator[Any, Any, Any]) -> dict[str, Any]: ...
def getcoroutinelocals(coroutine: Coroutine[Any, Any, Any]) -> dict[str, Any]: ...

View File

@@ -18,7 +18,7 @@ def ip_network(
address: _RawIPAddress | _RawNetworkPart | tuple[_RawIPAddress] | tuple[_RawIPAddress, int], strict: bool = True
) -> IPv4Network | IPv6Network: ...
def ip_interface(
address: _RawIPAddress | _RawNetworkPart | tuple[_RawIPAddress] | tuple[_RawIPAddress, int]
address: _RawIPAddress | _RawNetworkPart | tuple[_RawIPAddress] | tuple[_RawIPAddress, int],
) -> IPv4Interface | IPv6Interface: ...
class _IPAddressBase:

View File

@@ -2,11 +2,11 @@ from collections.abc import Callable, Iterator
from re import Pattern
from typing import Any, Final
ESCAPE: Final[Pattern[str]]
ESCAPE_ASCII: Final[Pattern[str]]
HAS_UTF8: Final[Pattern[bytes]]
ESCAPE_DCT: Final[dict[str, str]]
INFINITY: Final[float]
ESCAPE: Final[Pattern[str]] # undocumented
ESCAPE_ASCII: Final[Pattern[str]] # undocumented
HAS_UTF8: Final[Pattern[bytes]] # undocumented
ESCAPE_DCT: Final[dict[str, str]] # undocumented
INFINITY: Final[float] # undocumented
def py_encode_basestring(s: str) -> str: ... # undocumented
def py_encode_basestring_ascii(s: str) -> str: ... # undocumented

View File

@@ -1,3 +1,7 @@
from _json import make_scanner as make_scanner
from re import Pattern
from typing import Final
__all__ = ["make_scanner"]
NUMBER_RE: Final[Pattern[str]] # undocumented

View File

@@ -1,6 +1,6 @@
import sys
from collections.abc import Iterable
from typing import Protocol, SupportsFloat, SupportsIndex, TypeVar, overload
from typing import Final, Protocol, SupportsFloat, SupportsIndex, TypeVar, overload
from typing_extensions import TypeAlias
_T = TypeVar("_T")
@@ -8,11 +8,11 @@ _T_co = TypeVar("_T_co", covariant=True)
_SupportsFloatOrIndex: TypeAlias = SupportsFloat | SupportsIndex
e: float
pi: float
inf: float
nan: float
tau: float
e: Final[float]
pi: Final[float]
inf: Final[float]
nan: Final[float]
tau: Final[float]
def acos(x: _SupportsFloatOrIndex, /) -> float: ...
def acosh(x: _SupportsFloatOrIndex, /) -> float: ...

View File

@@ -1,8 +1,8 @@
import builtins
from _typeshed import Incomplete, MaybeNone
from _typeshed import MaybeNone, SupportsWrite
from abc import abstractmethod
from collections.abc import Callable, Iterable, Mapping, Sequence
from typing import IO, Any, AnyStr, ClassVar, Literal, NoReturn, overload
from typing import Any, ClassVar, Literal, NoReturn, overload
from typing_extensions import Self
__all__ = [
@@ -274,13 +274,13 @@ class OptionParser(OptionContainer):
def _add_version_option(self) -> None: ...
def _create_option_list(self) -> None: ...
def _get_all_options(self) -> list[Option]: ...
def _get_args(self, args: Iterable[Incomplete]) -> list[Incomplete]: ...
def _get_args(self, args: list[str] | None) -> list[str]: ...
def _init_parsing_state(self) -> None: ...
def _match_long_opt(self, opt: str) -> str: ...
def _populate_option_list(self, option_list: Iterable[Option], add_help: bool = True) -> None: ...
def _process_args(self, largs: list[Incomplete], rargs: list[Incomplete], values: Values) -> None: ...
def _process_long_opt(self, rargs: list[Incomplete], values) -> None: ...
def _process_short_opts(self, rargs: list[Incomplete], values) -> None: ...
def _populate_option_list(self, option_list: Iterable[Option] | None, add_help: bool = True) -> None: ...
def _process_args(self, largs: list[str], rargs: list[str], values: Values) -> None: ...
def _process_long_opt(self, rargs: list[str], values: Values) -> None: ...
def _process_short_opts(self, rargs: list[str], values: Values) -> None: ...
@overload
def add_option_group(self, opt_group: OptionGroup, /) -> OptionGroup: ...
@overload
@@ -299,14 +299,11 @@ class OptionParser(OptionContainer):
def get_prog_name(self) -> str: ...
def get_usage(self) -> str: ...
def get_version(self) -> str: ...
@overload
def parse_args(self, args: None = None, values: Values | None = None) -> tuple[Values, list[str]]: ...
@overload
def parse_args(self, args: Sequence[AnyStr], values: Values | None = None) -> tuple[Values, list[AnyStr]]: ...
def print_usage(self, file: IO[str] | None = None) -> None: ...
def print_help(self, file: IO[str] | None = None) -> None: ...
def print_version(self, file: IO[str] | None = None) -> None: ...
def set_default(self, dest, value) -> None: ...
def set_defaults(self, **kwargs) -> None: ...
def set_process_default_values(self, process) -> None: ...
def set_usage(self, usage: str) -> None: ...
def parse_args(self, args: list[str] | None = None, values: Values | None = None) -> tuple[Values, list[str]]: ...
def print_usage(self, file: SupportsWrite[str] | None = None) -> None: ...
def print_help(self, file: SupportsWrite[str] | None = None) -> None: ...
def print_version(self, file: SupportsWrite[str] | None = None) -> None: ...
def set_default(self, dest: str, value: Any) -> None: ... # default value can be "any" type
def set_defaults(self, **kwargs: Any) -> None: ... # default values can be "any" type
def set_process_default_values(self, process: bool) -> None: ...
def set_usage(self, usage: str | None) -> None: ...

View File

@@ -396,6 +396,7 @@ def intern(string: str, /) -> str: ...
if sys.version_info >= (3, 13):
def _is_gil_enabled() -> bool: ...
def _clear_internal_caches() -> None: ...
def is_finalizing() -> bool: ...
def breakpointhook(*args: Any, **kwargs: Any) -> Any: ...

View File

@@ -130,9 +130,8 @@ class Untokenizer:
if sys.version_info >= (3, 12):
def escape_brackets(self, token: str) -> str: ...
# the docstring says "returns bytes" but is incorrect --
# if the ENCODING token is missing, it skips the encode
def untokenize(iterable: Iterable[_Token]) -> Any: ...
# Returns str, unless the ENCODING token is present, in which case it returns bytes.
def untokenize(iterable: Iterable[_Token]) -> str | Any: ...
def detect_encoding(readline: Callable[[], bytes | bytearray]) -> tuple[str, Sequence[bytes]]: ...
def tokenize(readline: Callable[[], bytes | bytearray]) -> Generator[TokenInfo, None, None]: ...
def generate_tokens(readline: Callable[[], str]) -> Generator[TokenInfo, None, None]: ...

View File

@@ -640,6 +640,7 @@ if sys.version_info >= (3, 9):
def __getitem__(self, typeargs: Any, /) -> GenericAlias: ...
def __eq__(self, value: object, /) -> bool: ...
def __hash__(self) -> int: ...
def __mro_entries__(self, bases: Iterable[object], /) -> tuple[type, ...]: ...
if sys.version_info >= (3, 11):
@property
def __unpacked__(self) -> bool: ...

View File

@@ -29,6 +29,10 @@ console_error_panic_hook = { workspace = true, optional = true }
console_log = { workspace = true }
js-sys = { workspace = true }
log = { workspace = true }
# Not a direct dependency but required to enable the `wasm_js` feature.
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
getrandom = { workspace = true, features = ["wasm_js"] }
wasm-bindgen = { workspace = true }
[dev-dependencies]
@@ -36,3 +40,4 @@ wasm-bindgen-test = { workspace = true }
[lints]
workspace = true

View File

@@ -19,6 +19,15 @@ fn check() {
assert_eq!(
result,
vec!["error[lint:unresolved-import] /test.py:1:8 Cannot resolve import `random22`"]
vec![
"\
error: lint:unresolved-import
--> /test.py:1:8
|
1 | import random22
| ^^^^^^^^ Cannot resolve import `random22`
|
",
],
);
}

View File

@@ -18,7 +18,8 @@ fn lint_select() {
When breaking ties between enabled and disabled rules (via `select` and
`ignore`, respectively), more specific prefixes override less
specific prefixes.
specific prefixes. `ignore` takes precedence over `select` if the
same prefix appears in both.
Default value: ["E4", "E7", "E9", "F"]
Type: list[RuleSelector]
@@ -41,7 +42,7 @@ fn lint_select_json() {
exit_code: 0
----- stdout -----
{
"doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes.",
"doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes. `ignore` takes precedence over `select` if the\nsame prefix appears in both.",
"default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
"value_type": "list[RuleSelector]",
"scope": null,

View File

@@ -41,10 +41,6 @@ codspeed-criterion-compat = { workspace = true, default-features = false, option
criterion = { workspace = true, default-features = false }
rayon = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
url = { workspace = true }
ureq = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true }

View File

@@ -3,7 +3,10 @@ use std::path::Path;
use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_benchmark::{
TestCase, LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, UNICODE_PYPINYIN,
};
use ruff_python_formatter::{format_module_ast, PreviewMode, PyFormatOptions};
use ruff_python_parser::{parse, Mode};
use ruff_python_trivia::CommentRanges;
@@ -24,27 +27,20 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
fn create_test_cases() -> Result<Vec<TestCase>, TestFileDownloadError> {
Ok(vec![
TestCase::fast(TestFile::try_download("numpy/globals.py", "https://raw.githubusercontent.com/numpy/numpy/89d64415e349ca75a25250f22b874aa16e5c0973/numpy/_globals.py")?),
TestCase::fast(TestFile::try_download("unicode/pypinyin.py", "https://raw.githubusercontent.com/mozillazg/python-pinyin/9521e47d96e3583a5477f5e43a2e82d513f27a3f/pypinyin/standard.py")?),
TestCase::normal(TestFile::try_download(
"pydantic/types.py",
"https://raw.githubusercontent.com/pydantic/pydantic/83b3c49e99ceb4599d9286a3d793cea44ac36d4b/pydantic/types.py",
)?),
TestCase::normal(TestFile::try_download("numpy/ctypeslib.py", "https://raw.githubusercontent.com/numpy/numpy/e42c9503a14d66adfd41356ef5640c6975c45218/numpy/ctypeslib.py")?),
TestCase::slow(TestFile::try_download(
"large/dataset.py",
"https://raw.githubusercontent.com/DHI/mikeio/b7d26418f4db2909b0aa965253dbe83194d7bb5b/tests/test_dataset.py",
)?),
])
fn create_test_cases() -> Vec<TestCase> {
vec![
TestCase::fast(NUMPY_GLOBALS.clone()),
TestCase::fast(UNICODE_PYPINYIN.clone()),
TestCase::normal(PYDANTIC_TYPES.clone()),
TestCase::normal(NUMPY_CTYPESLIB.clone()),
TestCase::slow(LARGE_DATASET.clone()),
]
}
fn benchmark_formatter(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("formatter");
let test_cases = create_test_cases().unwrap();
for case in test_cases {
for case in create_test_cases() {
group.throughput(Throughput::Bytes(case.code().len() as u64));
group.bench_with_input(

View File

@@ -1,7 +1,9 @@
use ruff_benchmark::criterion::{
criterion_group, criterion_main, measurement::WallTime, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_benchmark::{
TestCase, LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, UNICODE_PYPINYIN,
};
use ruff_python_parser::{lexer, Mode, TokenKind};
#[cfg(target_os = "windows")]
@@ -20,24 +22,18 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
fn create_test_cases() -> Result<Vec<TestCase>, TestFileDownloadError> {
Ok(vec![
TestCase::fast(TestFile::try_download("numpy/globals.py", "https://raw.githubusercontent.com/numpy/numpy/89d64415e349ca75a25250f22b874aa16e5c0973/numpy/_globals.py")?),
TestCase::fast(TestFile::try_download("unicode/pypinyin.py", "https://raw.githubusercontent.com/mozillazg/python-pinyin/9521e47d96e3583a5477f5e43a2e82d513f27a3f/pypinyin/standard.py")?),
TestCase::normal(TestFile::try_download(
"pydantic/types.py",
"https://raw.githubusercontent.com/pydantic/pydantic/83b3c49e99ceb4599d9286a3d793cea44ac36d4b/pydantic/types.py",
)?),
TestCase::normal(TestFile::try_download("numpy/ctypeslib.py", "https://raw.githubusercontent.com/numpy/numpy/e42c9503a14d66adfd41356ef5640c6975c45218/numpy/ctypeslib.py")?),
TestCase::slow(TestFile::try_download(
"large/dataset.py",
"https://raw.githubusercontent.com/DHI/mikeio/b7d26418f4db2909b0aa965253dbe83194d7bb5b/tests/test_dataset.py",
)?),
])
fn create_test_cases() -> Vec<TestCase> {
vec![
TestCase::fast(NUMPY_GLOBALS.clone()),
TestCase::fast(UNICODE_PYPINYIN.clone()),
TestCase::normal(PYDANTIC_TYPES.clone()),
TestCase::normal(NUMPY_CTYPESLIB.clone()),
TestCase::slow(LARGE_DATASET.clone()),
]
}
fn benchmark_lexer(criterion: &mut Criterion<WallTime>) {
let test_cases = create_test_cases().unwrap();
let test_cases = create_test_cases();
let mut group = criterion.benchmark_group("lexer");
for case in test_cases {

View File

@@ -1,7 +1,9 @@
use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_benchmark::{
TestCase, LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, UNICODE_PYPINYIN,
};
use ruff_linter::linter::{lint_only, ParseSource};
use ruff_linter::rule_selector::PreviewOptions;
use ruff_linter::settings::rule_table::RuleTable;
@@ -46,24 +48,18 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[allow(unsafe_code)]
pub static _rjem_malloc_conf: &[u8] = b"dirty_decay_ms:-1,muzzy_decay_ms:-1\0";
fn create_test_cases() -> Result<Vec<TestCase>, TestFileDownloadError> {
Ok(vec![
TestCase::fast(TestFile::try_download("numpy/globals.py", "https://raw.githubusercontent.com/numpy/numpy/89d64415e349ca75a25250f22b874aa16e5c0973/numpy/_globals.py")?),
TestCase::fast(TestFile::try_download("unicode/pypinyin.py", "https://raw.githubusercontent.com/mozillazg/python-pinyin/9521e47d96e3583a5477f5e43a2e82d513f27a3f/pypinyin/standard.py")?),
TestCase::normal(TestFile::try_download(
"pydantic/types.py",
"https://raw.githubusercontent.com/pydantic/pydantic/83b3c49e99ceb4599d9286a3d793cea44ac36d4b/pydantic/types.py",
)?),
TestCase::normal(TestFile::try_download("numpy/ctypeslib.py", "https://raw.githubusercontent.com/numpy/numpy/e42c9503a14d66adfd41356ef5640c6975c45218/numpy/ctypeslib.py")?),
TestCase::slow(TestFile::try_download(
"large/dataset.py",
"https://raw.githubusercontent.com/DHI/mikeio/b7d26418f4db2909b0aa965253dbe83194d7bb5b/tests/test_dataset.py",
)?),
])
fn create_test_cases() -> Vec<TestCase> {
vec![
TestCase::fast(NUMPY_GLOBALS.clone()),
TestCase::fast(UNICODE_PYPINYIN.clone()),
TestCase::normal(PYDANTIC_TYPES.clone()),
TestCase::normal(NUMPY_CTYPESLIB.clone()),
TestCase::slow(LARGE_DATASET.clone()),
]
}
fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
let test_cases = create_test_cases().unwrap();
let test_cases = create_test_cases();
for case in test_cases {
group.throughput(Throughput::Bytes(case.code().len() as u64));

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