Compare commits

..

56 Commits

Author SHA1 Message Date
Micha Reiser
f6b2544993 Add rule registry and rule selection 2024-12-09 14:10:40 +01:00
Micha Reiser
fe78d50560 Add declare_lint 2024-12-09 14:08:19 +01:00
Micha Reiser
b39def2915 Introduce DiagnosticId 2024-12-09 14:05:09 +01:00
Micha Reiser
cf260aef2b Upgrade to react 19 (#14864)
## Summary

Upgrades to React 19. Closes
https://github.com/astral-sh/ruff/issues/14859

## Test Plan

I ran the playground locally and clicked through the different panels. I
didn't see any warning or error.
2024-12-09 10:15:38 +00:00
Dimitri Papadopoulos Orfanos
59145098d6 Fix typos found by codespell (#14863)
## Summary

Just fix typos.

## Test Plan

CI tests.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-09 09:32:12 +00:00
Harutaka Kawamura
3d9ac535e9 Fix pytest-parametrize-names-wrong-type (PT006) to edit both argnames and argvalues if both of them are single-element tuples/lists (#14699)
## Summary

Close #11243. Fix `pytest-parametrize-names-wrong-type (PT006)` to edit
both `argnames` and `argvalues` if both of them are single-element
tuples/lists.

```python
# Before fix
@pytest.mark.parametrize(("x",), [(1,), (2,)])
def test_foo(x):
    ...

# After fix:
@pytest.mark.parametrize("x", [1, 2])
def test_foo(x):
    ...
```

## Test Plan

New test cases
2024-12-09 09:58:52 +01:00
InSync
8df4983057 Promote uv in installation guides (#14056)
> [Because this is an Astral repository
;)](https://github.com/astral-sh/packse/pull/183)

[Originally
reported](https://discord.com/channels/1039017663004942429/1039017663512449056/1302319421204729906)
by clearfram3 on Discord.

`grep`ping for `pip install` in `.md` files reveals a few other places
where the same fix might be applicable.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-09 08:25:18 +00:00
renovate[bot]
56a631a868 Update pre-commit hook python-jsonschema/check-jsonschema to v0.30.0 (#14855)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[python-jsonschema/check-jsonschema](https://redirect.github.com/python-jsonschema/check-jsonschema)
| repository | minor | `0.29.4` -> `0.30.0` |

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>python-jsonschema/check-jsonschema
(python-jsonschema/check-jsonschema)</summary>

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

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

- Update vendored schemas: azure-pipelines, bitbucket-pipelines,
buildkite,
circle-ci, cloudbuild, dependabot, github-workflows, gitlab-ci, mergify,
    readthedocs, renovate, taskfile, woodpecker-ci (2024-11-29)
- Fix caching behavior to always use URL hashes as cache keys. This
fixes a
cache confusion bug in which the wrong schema could be retrieved from
the
cache. This resolves :cve:`2024-53848`. Thanks :user:`sethmlarson` for
reporting!
- Deprecate the `--cache-filename` flag. It no longer has any effect and
will
    be removed in a future release.

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 11:04:59 +05:30
Dylan
9d641fa714 [pylint] Include parentheses and multiple comparators in check for boolean-chained-comparison (PLR1716) (#14781)
This PR introduces three changes to the diagnostic and fix behavior
(still under preview) for [boolean-chained-comparison
(PLR1716)](https://docs.astral.sh/ruff/rules/boolean-chained-comparison/#boolean-chained-comparison-plr1716).

1. We now offer a _fix_ in the case of parenthesized expressions like
`(a < b) and b < c`. The fix will merge the chains of comparisons and
then balance parentheses by _adding_ parentheses to one side of the
expression.
2. We now trigger a diagnostic (and fix) in the case where some
comparisons have multiple comparators like `a < b < c and c < d`.
3. When adjacent comparators are parenthesized, we prefer the left
parenthesization and apply the replacement to the whole parenthesized
range. So, for example, `a < (b) and ((b)) < c` becomes `a < (b) < c`.

While these seem like somewhat disconnected changes, they are actually
related. If we only offered (1), then we would see the following fix
behavior:

```diff
- (a < b) and b < c and ((c < d))
+ (a < b < c) and ((c < d))
```

This is because the fix which add parentheses to the first pair of
comparisons overlaps with the fix that removes the `and` between the
second two comparisons. So the latter fix is deferred. However, the
latter fix does not get a second chance because, upon the next lint
iteration, there is no violation of `PLR1716`.

Upon adopting (2), however, both fixes occur by the time ruff completes
several iterations and we get:

```diff
- (a < b) and b < c and ((c < d))
+ ((a < b < c < d))
```

Finally, (3) fixes a previously unobserved bug wherein the autofix for
`a < (b) and b < c` used to result in `a<(b<c` which gives a syntax
error. It could in theory have been fixed in a separate PR, but seems to
be on theme here.


----------

- Closes #13524
- (1), (2), and (3) are implemented in separate commits for ease of
review and modification.
- Technically a user can trigger an error in ruff (by reaching max
iterations) if they have a humongous boolean chained comparison with
differing parentheses levels.
2024-12-08 22:58:45 -06:00
renovate[bot]
b56b3c813c Update NPM Development dependencies (#14854) 2024-12-08 20:58:30 -05:00
renovate[bot]
c59d54370b Update Rust crate tracing-indicatif to v0.3.8 (#14850) 2024-12-08 20:58:24 -05:00
renovate[bot]
dfaf2de81d Update Rust crate clap to v4.5.23 (#14846) 2024-12-08 20:58:14 -05:00
Dmitry Shachnev
d53e5cd25a [flake8-commas]: Fix example replacement in docs (#14843)
## Summary

Minor change for the documentation of COM818 rule. This was a block
called “In the event that a tuple is intended”, but the suggested change
did not produce a tuple.

## Test Plan

```python
>>> import json
>>> (json.dumps({"bar": 1}),)  # this is a tuple
('{"bar": 1}',)
>>> (json.dumps({"bar": 1}))  # not a tuple
'{"bar": 1}'
```
2024-12-09 00:51:59 +00:00
renovate[bot]
ed6de39725 Update Rust crate pep440_rs to v0.7.3 (#14848)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [pep440_rs](https://redirect.github.com/konstin/pep440-rs) |
workspace.dependencies | patch | `0.7.2` -> `0.7.3` |

---

### Release Notes

<details>
<summary>konstin/pep440-rs (pep440_rs)</summary>

###
[`v0.7.3`](https://redirect.github.com/konstin/pep440-rs/blob/HEAD/Changelog.md#073)

[Compare
Source](https://redirect.github.com/konstin/pep440-rs/compare/v0.7.2...v0.7.3)

-   Use once_cell to lower MSRV

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:48:52 +00:00
renovate[bot]
f4a7da7e93 Update Rust crate dir-test to v0.4.1 (#14847)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [dir-test](https://redirect.github.com/fe-lang/dir-test) |
workspace.dependencies | patch | `0.4.0` -> `0.4.1` |

---

### Release Notes

<details>
<summary>fe-lang/dir-test (dir-test)</summary>

###
[`v0.4.1`](https://redirect.github.com/fe-lang/dir-test/releases/tag/v0.4.1)

[Compare
Source](https://redirect.github.com/fe-lang/dir-test/compare/v0.4.0...v0.4.1)

#### What's Changed

- include license texts in published crates by
[@&#8203;decathorpe](https://redirect.github.com/decathorpe) in
[https://github.com/fe-lang/dir-test/pull/12](https://redirect.github.com/fe-lang/dir-test/pull/12)

**Full Changelog**:
https://github.com/fe-lang/dir-test/compare/v0.4.0...v0.4.1

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:48:06 +00:00
renovate[bot]
8bf04988fb Update Rust crate thiserror to v2.0.6 (#14849)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

- Suppress deprecation warning on generated From impls
([#&#8203;396](https://redirect.github.com/dtolnay/thiserror/issues/396))

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

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

- Prevent deprecation warning on generated impl for deprecated type
([#&#8203;394](https://redirect.github.com/dtolnay/thiserror/issues/394))

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

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

- Eliminate needless_lifetimes clippy lint in generated `From` impls
([#&#8203;391](https://redirect.github.com/dtolnay/thiserror/issues/391),
thanks [@&#8203;matt-phylum](https://redirect.github.com/matt-phylum))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:47:09 +00:00
renovate[bot]
745a4b425e Update dependency ruff to v0.8.2 (#14851)
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.8.1` -> `==0.8.2` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/ruff/0.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/ruff/0.8.1/0.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.8.1/0.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

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

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

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

##### Preview features

- \[`airflow`] Avoid deprecated values (`AIR302`)
([#&#8203;14582](https://redirect.github.com/astral-sh/ruff/pull/14582))
- \[`airflow`] Extend removed names for `AIR302`
([#&#8203;14734](https://redirect.github.com/astral-sh/ruff/pull/14734))
- \[`ruff`] Extend `unnecessary-regular-expression` to non-literal
strings (`RUF055`)
([#&#8203;14679](https://redirect.github.com/astral-sh/ruff/pull/14679))
- \[`ruff`] Implement `used-dummy-variable` (`RUF052`)
([#&#8203;14611](https://redirect.github.com/astral-sh/ruff/pull/14611))
- \[`ruff`] Implement `unnecessary-cast-to-int` (`RUF046`)
([#&#8203;14697](https://redirect.github.com/astral-sh/ruff/pull/14697))

##### Rule changes

- \[`airflow`] Check `AIR001` from builtin or providers `operators`
module
([#&#8203;14631](https://redirect.github.com/astral-sh/ruff/pull/14631))
- \[`flake8-pytest-style`] Remove `@` in `pytest.mark.parametrize` rule
messages
([#&#8203;14770](https://redirect.github.com/astral-sh/ruff/pull/14770))
- \[`pandas-vet`] Skip rules if the `panda` module hasn't been seen
([#&#8203;14671](https://redirect.github.com/astral-sh/ruff/pull/14671))
- \[`pylint`] Fix false negatives for `ascii` and `sorted` in
`len-as-condition` (`PLC1802`)
([#&#8203;14692](https://redirect.github.com/astral-sh/ruff/pull/14692))
- \[`refurb`] Guard `hashlib` imports and mark `hashlib-digest-hex` fix
as safe (`FURB181`)
([#&#8203;14694](https://redirect.github.com/astral-sh/ruff/pull/14694))

##### Configuration

- \[`flake8-import-conventions`] Improve syntax check for aliases
supplied in configuration for `unconventional-import-alias` (`ICN001`)
([#&#8203;14745](https://redirect.github.com/astral-sh/ruff/pull/14745))

##### Bug fixes

- Revert: \[pyflakes] Avoid false positives in `@no_type_check` contexts
(`F821`, `F722`)
([#&#8203;14615](https://redirect.github.com/astral-sh/ruff/issues/14615))
([#&#8203;14726](https://redirect.github.com/astral-sh/ruff/pull/14726))
- \[`pep8-naming`] Avoid false positive for `class Bar(type(foo))`
(`N804`)
([#&#8203;14683](https://redirect.github.com/astral-sh/ruff/pull/14683))
- \[`pycodestyle`] Handle f-strings properly for
`invalid-escape-sequence` (`W605`)
([#&#8203;14748](https://redirect.github.com/astral-sh/ruff/pull/14748))
- \[`pylint`] Ignore `@overload` in `PLR0904`
([#&#8203;14730](https://redirect.github.com/astral-sh/ruff/pull/14730))
- \[`refurb`] Handle non-finite decimals in
`verbose-decimal-constructor` (`FURB157`)
([#&#8203;14596](https://redirect.github.com/astral-sh/ruff/pull/14596))
- \[`ruff`] Avoid emitting `assignment-in-assert` when all references to
the assigned variable are themselves inside `assert`s (`RUF018`)
([#&#8203;14661](https://redirect.github.com/astral-sh/ruff/pull/14661))

##### Documentation

- Improve docs for `flake8-use-pathlib` rules
([#&#8203;14741](https://redirect.github.com/astral-sh/ruff/pull/14741))
- Improve error messages and docs for `flake8-comprehensions` rules
([#&#8203;14729](https://redirect.github.com/astral-sh/ruff/pull/14729))
- \[`flake8-type-checking`] Expands `TC006` docs to better explain
itself
([#&#8203;14749](https://redirect.github.com/astral-sh/ruff/pull/14749))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:46:20 +00:00
renovate[bot]
dfab134bb6 Update Rust crate ureq to v2.12.1 (#14853)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ureq](https://redirect.github.com/algesten/ureq) |
workspace.dependencies | minor | `2.11.0` -> `2.12.1` |

---

### Release Notes

<details>
<summary>algesten/ureq (ureq)</summary>

###
[`v2.12.1`](https://redirect.github.com/algesten/ureq/blob/HEAD/CHANGELOG.md#2121)

[Compare
Source](https://redirect.github.com/algesten/ureq/compare/2.12.0...2.12.1)

- Do not use multi-version deps (>=x.x.x)
([#&#8203;907](https://redirect.github.com/algesten/ureq/issues/907))

###
[`v2.12.0`](https://redirect.github.com/algesten/ureq/blob/HEAD/CHANGELOG.md#2120)

[Compare
Source](https://redirect.github.com/algesten/ureq/compare/2.11.0...2.12.0)

- Bump MSRV 1.67 -> 1.71 because rustls will soon adopt it
([#&#8203;905](https://redirect.github.com/algesten/ureq/issues/905))
- Unpin rustls dep (>=0.23.19)
([#&#8203;905](https://redirect.github.com/algesten/ureq/issues/905))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:46:05 +00:00
Alex Waygood
58e7db89a1 Run zizmor in CI, and fix most warnings (#14844)
## Summary

A [recent exploit](https://github.com/advisories/GHSA-7x29-qqmq-v6qc)
brought attention to how easy it can be for attackers to use template
expansion in GitHub Actions workflows to inject arbitrary code into a
repository. That vulnerability [would have been caught by the zizmor
linter](https://blog.yossarian.net/2024/12/06/zizmor-ultralytics-injection),
which looks for potential security vulnerabilities in GitHub Actions
workflows. This PR adds [zizmor](https://github.com/woodruffw/zizmor) as
a pre-commit hook and fixes the high- and medium-severity warnings
flagged by the tool.

All the warnings fixed in this PR are related to this zizmor check:
https://woodruffw.github.io/zizmor/audits/#artipacked. The summary of
the check is that `actions/checkout` will by default persist git
configuration for the duration of the workflow, which can be insecure.
It's unnecessary unless you actually need to do things with `git` later
on in the workflow. None of our workflows do except for
`publish-docs.yml` and `sync-typeshed.yml`, so I set
`persist-credentials: true` for those two but `persist-credentials:
false` for all other uses of `actions/checkout`.

Unfortunately there are several warnings in `release.yml`, including
four high-severity warnings. However, this is a generated workflow file,
so I have deliberately excluded this file from the check. These are the
findings in `release.yml`:

<details>
<summary>release.yml findings</summary>

```
warning[artipacked]: credential persistence through GitHub Actions artifacts
  --> /Users/alexw/dev/ruff/.github/workflows/release.yml:62:9
   |
62 |         - uses: actions/checkout@v4
   |  _________-
63 | |         with:
64 | |           submodules: recursive
   | |_______________________________- does not set persist-credentials: false
   |
   = note: audit confidence → Low

warning[artipacked]: credential persistence through GitHub Actions artifacts
   --> /Users/alexw/dev/ruff/.github/workflows/release.yml:124:9
    |
124 |         - uses: actions/checkout@v4
    |  _________-
125 | |         with:
126 | |           submodules: recursive
    | |_______________________________- does not set persist-credentials: false
    |
    = note: audit confidence → Low

warning[artipacked]: credential persistence through GitHub Actions artifacts
   --> /Users/alexw/dev/ruff/.github/workflows/release.yml:174:9
    |
174 |         - uses: actions/checkout@v4
    |  _________-
175 | |         with:
176 | |           submodules: recursive
    | |_______________________________- does not set persist-credentials: false
    |
    = note: audit confidence → Low

warning[artipacked]: credential persistence through GitHub Actions artifacts
   --> /Users/alexw/dev/ruff/.github/workflows/release.yml:249:9
    |
249 |         - uses: actions/checkout@v4
    |  _________-
250 | |         with:
251 | |           submodules: recursive
252 | |       # Create a GitHub Release while uploading all files to it
    | |_______________________________________________________________- does not set persist-credentials: false
    |
    = note: audit confidence → Low

error[excessive-permissions]: overly broad workflow or job-level permissions
  --> /Users/alexw/dev/ruff/.github/workflows/release.yml:17:1
   |
17 | / permissions:
18 | |   "contents": "write"
...  |
39 | | # If there's a prerelease-style suffix to the version, then the release(s)
40 | | # will be marked as a prerelease.
   | |_________________________________^ contents: write is overly broad at the workflow level
   |
   = note: audit confidence → High

error[template-injection]: code injection via template expansion
  --> /Users/alexw/dev/ruff/.github/workflows/release.yml:80:9
   |
80 |          - id: plan
   |   _________^
81 |  |         run: |
   |  |_________^
82 | ||           dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --out...
83 | ||           echo "dist ran successfully"
84 | ||           cat plan-dist-manifest.json
85 | ||           echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
   | ||__________________________________________________________________________________^ this step
   | ||__________________________________________________________________________________^ inputs.tag may expand into attacker-controllable code
   |
   = note: audit confidence → Low

error[template-injection]: code injection via template expansion
  --> /Users/alexw/dev/ruff/.github/workflows/release.yml:80:9
   |
80 |          - id: plan
   |   _________^
81 |  |         run: |
   |  |_________^
82 | ||           dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --out...
83 | ||           echo "dist ran successfully"
84 | ||           cat plan-dist-manifest.json
85 | ||           echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
   | ||__________________________________________________________________________________^ this step
   | ||__________________________________________________________________________________^ inputs.tag may expand into attacker-controllable code
   |
   = note: audit confidence → Low

error[template-injection]: code injection via template expansion
  --> /Users/alexw/dev/ruff/.github/workflows/release.yml:80:9
   |
80 |          - id: plan
   |   _________^
81 |  |         run: |
   |  |_________^
82 | ||           dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --out...
83 | ||           echo "dist ran successfully"
84 | ||           cat plan-dist-manifest.json
85 | ||           echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
   | ||__________________________________________________________________________________^ this step
   | ||__________________________________________________________________________________^ inputs.tag may expand into attacker-controllable code
   |
   = note: audit confidence → Low
```

</details>

## Test Plan

`uvx pre-commit run -a`
2024-12-09 00:42:06 +00:00
renovate[bot]
8c4b22964e Update pre-commit dependencies (#14852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:13:17 +00:00
renovate[bot]
ecd948a083 Update Rust crate anyhow to v1.0.94 (#14845)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:12:06 +00:00
Alex Waygood
9b8ceb9a2e [pyupgrade] Mark fixes for convert-typed-dict-functional-to-class and convert-named-tuple-functional-to-class as unsafe if they will remove comments (UP013, UP014) (#14842) 2024-12-08 18:51:37 +00:00
Thibaut Decombe
8d9e408dbb Fix PLW1508 false positive for default string created via a mult operation (#14841) 2024-12-08 18:25:47 +00:00
ABDULRAHMAN ALRAHMA
85402097fc Improve error messages for except* (B025, B029, B030, B904) #14791 (#14815)
Improves error message for [except*](https://peps.python.org/pep-0654/)
(Rules: B025, B029, B030, B904)

Example python snippet:
```python
try:
    a = 1
except* ValueError:
    a = 2
except* ValueError:
    a = 2

try:
    pass
except* ():
    pass

try:
    pass
except* 1:  # error
    pass

try:
    raise ValueError
except* ValueError:
    raise UserWarning
```
Error messages
Before:
```
$ ruff check --select=B foo.py
foo.py:6:9: B025 try-except block with duplicate exception `ValueError`
foo.py:11:1: B029 Using `except ():` with an empty tuple does not catch anything; add exceptions to handle
foo.py:16:9: B030 `except` handlers should only be exception classes or tuples of exception classes
foo.py:22:5: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
Found 4 errors.
```
After:
```
$ ruff check --select=B foo.py
foo.py:6:9: B025 try-except* block with duplicate exception `ValueError`
foo.py:11:1: B029 Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle
foo.py:16:9: B030 `except*` handlers should only be exception classes or tuples of exception classes
foo.py:22:5: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
Found 4 errors.
```

Closes https://github.com/astral-sh/ruff/issues/14791

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-08 17:37:34 +00:00
Shaygan Hooshyari
269e47be96 Understand type[A | B] special form in annotations (#14830)
resolves https://github.com/astral-sh/ruff/issues/14703

I decided to use recursion to get the type, so if anything is added to
the single element inference it will be applied for the union.
Also added this
[change](https://github.com/astral-sh/ruff/issues/14703#issuecomment-2510286217)
in this PR since it was easy.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-07 17:34:50 +00:00
Dylan
d34013425f [flake8-bugbear] Offer unsafe autofix for no-explicit-stacklevel (B028) (#14829)
This PR introduces an unsafe autofix for [no-explicit-stacklevel
(B028)](https://docs.astral.sh/ruff/rules/no-explicit-stacklevel/#no-explicit-stacklevel-b028):
we add the `stacklevel` argument, set to `2`.

Closes #14805
2024-12-07 08:24:37 -05:00
Dylan
2c13e6513d [flake8-comprehensions] Skip iterables with named expressions in unnecessary-map (C417) (#14827)
This PR modifies [unnecessary-map
(C417)](https://docs.astral.sh/ruff/rules/unnecessary-map/#unnecessary-map-c417)
to skip `map` expressions if the iterable contains a named expression,
since those cannot appear in comprehensions.

Closes #14808
2024-12-06 22:00:33 -05:00
Douglas Creager
8fdd88013d Support type[a.X] with qualified class names (#14825)
This adds support for `type[a.X]`, where the `type` special form is
applied to a qualified name that resolves to a class literal. This works
for both nested classes and classes imported from another module.

Closes #14545
2024-12-06 17:14:51 -05:00
Carl Meyer
3017b3b687 [red-knot] function parameter types (#14802)
## Summary

Inferred and declared types for function parameters, in the function
body scope.

Fixes #13693.

## Test Plan

Added mdtests.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-06 12:55:56 -08:00
Alex Waygood
2119dcab6f [ruff] Teach autofix for used-dummy-variable about TypeVars etc. (RUF052) (#14819) 2024-12-06 17:05:50 +00:00
Wei Lee
3ea14d7a74 [airflow]: extend removed args (AIR302) (#14765)
## Summary

Airflow 3.0 removes various deprecated functions, members, modules, and
other values. They have been deprecated in 2.x, but the removal causes
incompatibilities that we want to detect. This PR deprecates the
following names.

* in `DAG`
    * `sla_miss_callback` was removed
* in `airflow.operators.trigger_dagrun.TriggerDagRunOperator`
    * `execution_date` was removed
* in `airflow.operators.weekday.DayOfWeekSensor`,
`airflow.operators.datetime.BranchDateTimeOperator` and
`airflow.operators.weekday.BranchDayOfWeekOperator`
* `use_task_execution_day` was removed in favor of
`use_task_logical_date`

The full list of rules we will extend
https://github.com/apache/airflow/issues/44556

## Test Plan

<!-- How was it tested? -->
A test fixture is included in the PR.
2024-12-06 17:00:23 +01:00
Alex Waygood
4fdd4ddfaa [ruff] Don't emit used-dummy-variable on function parameters (RUF052) (#14818) 2024-12-06 14:54:42 +00:00
David Peter
6b9f3d7d7c [red-knot] Import LiteralString/Never from typing_extensions (#14817)
## Summary

`typing.Never` and `typing.LiteralString` are only conditionally
exported from `typing` for Python versions 3.11 and later. We run the
Markdown tests with the default Python version of 3.9, so here we change
the import to `typing_extensions` instead, and add a new test to make
sure we'll continue to understand the `typing`-version of these symbols
for newer versions.

This didn't cause problems so far, as we don't understand
`sys.version_info` branches yet.

## Test Plan

New Markdown tests to make sure this will continue to work in the
future.
2024-12-06 13:57:51 +01:00
Alex Waygood
4cb8392523 Further simplifications to PTH210 (#14816) 2024-12-06 12:52:26 +00:00
Alex Waygood
9ee438b02f Minor nitpicks for PTH210 (#14814) 2024-12-06 12:23:21 +00:00
InSync
89368a62a8 [flake8-use-pathlib] Dotless suffix passed to Path.with_suffix() (PTH901) (#14779)
## Summary

Resolves #14441.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-12-06 13:08:20 +01:00
Micha Reiser
1559c73fcd Fix fstring formatting removing overlong implicit concatenated string in expression part (#14811)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/14778


The formatter incorrectly removed the inner implicitly concatenated
string for following single-line f-string:

```py
f"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}"

# formatted
f"{ if True else ''}"
```

This happened because I changed the `RemoveSoftlinesBuffer` in
https://github.com/astral-sh/ruff/pull/14489 to remove any content
wrapped in `if_group_breaks`. After all, it emulates an *all flat*
layout. This works fine when `if_group_breaks` is only used to **add**
content if the gorup breaks. It doesn't work if the same content is
rendered differently depending on if the group fits using
`if_group_breaks` and `if_groups_fits` because the enclosing `group`
might still *break* if the entire content exceeds the line-length limit.

This PR fixes this by unwrapping any `if_group_fits` content by removing
the `if_group_fits` start and end tags.


## Test Plan

added test
2024-12-06 13:01:04 +01:00
Wei Lee
39623f8d40 [airflow]: extend removed names (AIR302) (#14804)
## Summary

Airflow 3.0 removes various deprecated functions, members, modules, and
other values. They have been deprecated in 2.x, but the removal causes
incompatibilities that we want to detect. This PR deprecates the
following names.
The full list of rules we will extend
https://github.com/apache/airflow/issues/44556


#### package
* `airflow.contrib.*` 

#### module
* `airflow.operators.subdag.*` 

#### class
* `airflow.sensors.external_task.ExternalTaskSensorLink` →
`airflow.sensors.external_task.ExternalDagLin`
* `airflow.operators.bash_operator.BashOperator` →
`airflow.operators.bash.BashOperator`
* `airflow.operators.branch_operator.BaseBranchOperator` →
`airflow.operators.branch.BaseBranchOperator`
* `airflow.operators.dummy.EmptyOperator` →
`airflow.operators.empty.EmptyOperator`
* `airflow.operators.dummy.DummyOperator` →
`airflow.operators.empty.EmptyOperator`
* `airflow.operators.dummy_operator.EmptyOperator` →
`airflow.operators.empty.EmptyOperator`
* `airflow.operators.dummy_operator.DummyOperator` →
`airflow.operators.empty.EmptyOperator`
* `airflow.operators.email_operator.EmailOperator` →
`airflow.operators.email.EmailOperator`
* `airflow.sensors.base_sensor_operator.BaseSensorOperator` →
`airflow.sensors.base.BaseSensorOperator`
* `airflow.sensors.date_time_sensor.DateTimeSensor` →
`airflow.sensors.date_time.DateTimeSensor`
* `airflow.sensors.external_task_sensor.ExternalTaskMarker` →
`airflow.sensors.external_task.ExternalTaskMarker`
* `airflow.sensors.external_task_sensor.ExternalTaskSensor` →
`airflow.sensors.external_task.ExternalTaskSensor`
* `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` →
`airflow.sensors.external_task.ExternalTaskSensorLink`
* `airflow.sensors.time_delta_sensor.TimeDeltaSensor` →
`airflow.sensors.time_delta.TimeDeltaSensor`

#### function
* `airflow.utils.decorators.apply_defaults`
* `airflow.www.utils.get_sensitive_variables_fields` →
`airflow.utils.log.secrets_masker.get_sensitive_variables_fields`
* `airflow.www.utils.should_hide_value_for_key` →
`airflow.utils.log.secrets_masker.should_hide_value_for_key`
* `airflow.configuration.get` → `airflow.configuration.conf.get` 
* `airflow.configuration.getboolean` →
`airflow.configuration.conf.getboolean`
* `airflow.configuration.getfloat` →
`airflow.configuration.conf.getfloat`
* `airflow.configuration.getint` → `airflow.configuration.conf.getint` 
* `airflow.configuration.has_option` →
`airflow.configuration.conf.has_option`
* `airflow.configuration.remove_option` →
`airflow.configuration.conf.remove_option`
* `airflow.configuration.as_dict` → `airflow.configuration.conf.as_dict`
* `airflow.configuration.set` → `airflow.configuration.conf.set` 
* `airflow.secrets.local_filesystem.load_connections` →
`airflow.secrets.local_filesystem.load_connections_dict`
* `airflow.secrets.local_filesystem.get_connection` →
`airflow.secrets.local_filesystem.load_connections_dict`
* `airflow.utils.helpers.chain` → `airflow.models.baseoperator.chain` 
* `airflow.utils.helpers.cross_downstream` →
`airflow.models.baseoperator.cross_downstream`

#### attribute
* in `airflow.utils.trigger_rule.TriggerRule`
    * `DUMMY` 
    * `NONE_FAILED_OR_SKIPPED` 

#### constant / variable
* `airflow.PY\d\d`
2024-12-06 11:34:48 +01:00
Douglas Creager
918358aaa6 Migrate some inference tests to mdtests (#14795)
As part of #13696, this PR ports a smallish number of inference tests
over to the mdtest framework.
2024-12-06 11:19:22 +01:00
David Peter
b01a651e69 [red-knot] Support for TOML configs in Markdown tests (#14785)
## Summary

This adds support for specifying the target Python version from a
Markdown test. It is a somewhat limited ad-hoc solution, but designed to
be future-compatible. TOML blocks can be added to arbitrary sections in
the Markdown block. They have the following format:

````markdown
```toml
[tool.knot.environment]
target-version = "3.13"
```
````

So far, there is nothing else that can be configured, but it should be
straightforward to extend this to things like a custom typeshed path.

This is in preparation for the statically-known branches feature where
we are going to have to specify the target version for lots of tests.

## Test Plan

- New Markdown test that fails without the explicitly specified
`target-version`.
- Manually tested various error paths when specifying a wrong
`target-version` field.
- Made sure that running tests is as fast as before.
2024-12-06 10:22:08 +01:00
Micha Reiser
56afb12ae7 Fix infinite watch loop by ignoring 'uninteresting' watch events (#14809)
## Summary
Fixes https://github.com/astral-sh/ruff/issues/14807

I suspect that this broke when we updated notify, although I'm not quiet
sure how this *ever* worked...

The problem was that the file watcher didn't skip over `Access` events,
but Ruff itself accesses the `pyproject.toml` when checking the project.
That means, Ruff triggers `Access` events but it also schedules a
re-check on every `Access` event... and this goes one forever.

This PR skips over `Access` and `Other` event. `Access` events are
uninteresting because they're only reads, they don't change any file
metadata or content.
The `Other` events should be rare and are mainly to inform about file
watcher changes... we don't need those.

I also added an explicit handling for the `Rescan` event. File watchers
emit a `Rescan` event if they failed to capture some file watching
changes
and it signals that the program should assume that all files might have
changed (the program should do a rescan to *get up to date*).

## Test Plan

I tested that Ruff no longer loops when running `check --watch`. I
verified that Ruff rechecks file after making content changes.
2024-12-06 08:50:29 +00:00
Maksim Bondarenkov
b42e528555 deps: Update cc (#14794)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

in unknown moment older versions became broken for windows-gnullvm
targets. this update shouldn't break anything

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

## Test Plan

successfully built for windows-gnullvm with `cargo build`

<!-- How was it tested? -->
2024-12-05 21:47:14 -05:00
Christian Clauss
5aab57b3e9 docs/integrations.md: Upgrade example to astral-sh/ruff-action@v2 (#14800)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

https://docs.astral.sh/ruff/integrations/#github-actions upgraded for
https://github.com/astral-sh/ruff-action/releases

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

## Test Plan

<!-- How was it tested? -->
@eifinger Your review, please.
2024-12-05 21:47:03 -05:00
Dhruv Manilawala
40b0b67dd9 [red-knot] Separate invalid syntax code snippets (#14803)
Ref: https://github.com/astral-sh/ruff/pull/14788#discussion_r1872242283

This PR:
* Separates code snippets as individual tests for the invalid syntax
cases
* Adds a general comment explaining why the parser could emit more
syntax errors than expected
2024-12-06 02:41:33 +00:00
Dylan
1bd8fbb6e8 [flake8-pyi] Skip all type definitions in string-or-bytes-too-long (PYI053) (#14797) 2024-12-05 18:48:54 -06:00
Dhruv Manilawala
b0e26e6fc8 Bump version to 0.8.2 (#14789) 2024-12-05 18:06:35 +05:30
Dhruv Manilawala
e9941cd714 [red-knot] Move standalone expr inference to for non-name target (#14788)
## Summary

Ref: https://github.com/astral-sh/ruff/pull/14754#discussion_r1871040646

## Test Plan

Remove the TODO comment and update the mdtest.
2024-12-05 18:06:20 +05:30
Dhruv Manilawala
43bf1a8907 Add tests for "keyword as identifier" syntax errors (#14754)
## Summary

This is related to #13778, more specifically
https://github.com/astral-sh/ruff/issues/13778#issuecomment-2513556004.

This PR adds various test cases where a keyword is being where an
identifier is expected. The tests are to make sure that red knot doesn't
panic, raises the syntax error and the identifier is added to the symbol
table. The final part allows editor related features like renaming the
symbol.
2024-12-05 17:32:48 +05:30
InSync
fda8b1f884 [ruff] Unnecessary cast to int (RUF046) (#14697)
## Summary

Resolves #11412.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-05 10:30:06 +01:00
David Peter
2d3f557875 [red-knot] Fallback for typing._NoDefaultType (#14783)
## Summary

`typing_extensions` has a `>=3.13` re-export for the `typing.NoDefault`
singleton, but not for `typing._NoDefaultType`. This causes problems as
soon as we understand `sys.version_info` branches, so we explicity
switch to `typing._NoDefaultType` for Python 3.13 and later.

This is a part of #14759 that I thought might make sense to break out
and merge in isolation.

## Test Plan

New test that will become more meaningful with #12700

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-05 09:17:55 +01:00
David Peter
bd27bfab5d [red-knot] Unify setup_db() functions, add TestDb builder (#14777)
## Summary

- Instead of seven (more or less similar) `setup_db` functions, use just
one in a single central place.
- For every test that needs customization beyond that, offer a
`TestDbBuilder` that can control the Python target version, custom
typeshed, and pre-existing files.

The main motivation for this is that we're soon going to need
customization of the Python version, and I didn't feel like adding this
to each of the existing `setup_db` functions.
2024-12-04 21:36:54 +01:00
InSync
155d34bbb9 [red-knot] Infer precise types for len() calls (#14599)
## Summary

Resolves #14598.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-04 11:16:53 -08:00
Well404
04c887c8fc Fix references for async-busy-wait (#14775) 2024-12-04 18:05:49 +01:00
David Peter
af43bd4b0f [red-knot] Gradual forms do not participate in equivalence/subtyping (#14758)
## Summary

This changeset contains various improvements concerning non-fully-static
types and their relationships:

- Make sure that non-fully-static types do not participate in
equivalence or subtyping.
- Clarify what `Type::is_equivalent_to` actually implements.
- Introduce `Type::is_fully_static`
- New tests making sure that multiple `Any`/`Unknown`s inside unions and
intersections are collapsed.

closes #14524

## Test Plan

- Added new unit tests for union and intersection builder
- Added new unit tests for `Type::is_equivalent_to`
- Added new unit tests for `Type::is_subtype_of`
- Added new property test making sure that non-fully-static types do not
participate in subtyping
2024-12-04 17:11:25 +01:00
Harutaka Kawamura
614917769e Remove @ in pytest.mark.parametrize rule messages (#14770) 2024-12-04 16:01:10 +01:00
Douglas Creager
8b23086eac [red-knot] Add typing.Any as a spelling for the Any type (#14742)
We already had a representation for the Any type, which we would use
e.g. for expressions without type annotations. We now recognize
`typing.Any` as a way to refer to this type explicitly. Like other
special forms, this is tracked correctly through aliasing, and isn't
confused with local definitions that happen to have the same name.

Closes #14544
2024-12-04 09:56:36 -05:00
196 changed files with 9590 additions and 2413 deletions

View File

@@ -40,6 +40,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -68,6 +69,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -109,6 +111,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -164,6 +167,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -216,6 +220,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -290,6 +295,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -354,6 +360,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -419,6 +426,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

View File

@@ -36,6 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: docker/setup-buildx-action@v3

View File

@@ -38,6 +38,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: tj-actions/changed-files@v45
id: changed
@@ -99,6 +100,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- run: cargo fmt --all --check
@@ -111,6 +114,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: |
rustup component add clippy
@@ -129,6 +134,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -173,6 +180,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -200,6 +209,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
@@ -224,6 +235,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
@@ -251,6 +264,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -267,6 +282,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: SebRollen/toml-action@v1.2.0
id: msrv
with:
@@ -299,6 +316,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
@@ -325,6 +344,8 @@ jobs:
FORCE_COLOR: 1
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v4
- uses: actions/download-artifact@v4
name: Download Ruff binary to test
@@ -355,6 +376,8 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
@@ -379,6 +402,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -489,6 +514,8 @@ jobs:
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@main
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -499,6 +526,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -524,6 +553,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -555,6 +586,8 @@ jobs:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.13"
@@ -595,6 +628,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Cache rust"
@@ -622,6 +657,7 @@ jobs:
- uses: actions/checkout@v4
name: "Download ruff-lsp source"
with:
persist-credentials: false
repository: "astral-sh/ruff-lsp"
- uses: actions/setup-python@v5
@@ -657,6 +693,8 @@ jobs:
steps:
- name: "Checkout Branch"
uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show

View File

@@ -32,6 +32,8 @@ jobs:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v4
- name: "Install Rust toolchain"
run: rustup show

View File

@@ -26,6 +26,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
persist-credentials: true
- uses: actions/setup-python@v5
with:

View File

@@ -25,6 +25,8 @@ jobs:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4

View File

@@ -30,6 +30,8 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: jetli/wasm-pack-action@v0.4.0

View File

@@ -25,11 +25,13 @@ jobs:
name: Checkout Ruff
with:
path: ruff
persist-credentials: true
- uses: actions/checkout@v4
name: Checkout typeshed
with:
repository: python/typeshed
path: typeshed
persist-credentials: true
- name: Setup git
run: |
git config --global user.name typeshedbot

View File

@@ -59,7 +59,7 @@ repos:
- black==24.10.0
- repo: https://github.com/crate-ci/typos
rev: v1.28.1
rev: v1.28.2
hooks:
- id: typos
@@ -73,7 +73,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.1
rev: v0.8.2
hooks:
- id: ruff-format
- id: ruff
@@ -83,10 +83,25 @@ repos:
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.4.1
rev: v3.4.2
hooks:
- id: prettier
types: [yaml]
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v0.8.0
hooks:
- id: zizmor
# `release.yml` is autogenerated by `dist`; security issues need to be fixed there
# (https://opensource.axo.dev/cargo-dist/)
exclude: .github/workflows/release.yml
# We could consider enabling the low-severity warnings, but they're noisy
args: [--min-severity=medium]
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.30.0
hooks:
- id: check-github-workflows
ci:
skip: [cargo-fmt, dev-generate-all]

View File

@@ -1,5 +1,42 @@
# Changelog
## 0.8.2
### Preview features
- \[`airflow`\] Avoid deprecated values (`AIR302`) ([#14582](https://github.com/astral-sh/ruff/pull/14582))
- \[`airflow`\] Extend removed names for `AIR302` ([#14734](https://github.com/astral-sh/ruff/pull/14734))
- \[`ruff`\] Extend `unnecessary-regular-expression` to non-literal strings (`RUF055`) ([#14679](https://github.com/astral-sh/ruff/pull/14679))
- \[`ruff`\] Implement `used-dummy-variable` (`RUF052`) ([#14611](https://github.com/astral-sh/ruff/pull/14611))
- \[`ruff`\] Implement `unnecessary-cast-to-int` (`RUF046`) ([#14697](https://github.com/astral-sh/ruff/pull/14697))
### Rule changes
- \[`airflow`\] Check `AIR001` from builtin or providers `operators` module ([#14631](https://github.com/astral-sh/ruff/pull/14631))
- \[`flake8-pytest-style`\] Remove `@` in `pytest.mark.parametrize` rule messages ([#14770](https://github.com/astral-sh/ruff/pull/14770))
- \[`pandas-vet`\] Skip rules if the `panda` module hasn't been seen ([#14671](https://github.com/astral-sh/ruff/pull/14671))
- \[`pylint`\] Fix false negatives for `ascii` and `sorted` in `len-as-condition` (`PLC1802`) ([#14692](https://github.com/astral-sh/ruff/pull/14692))
- \[`refurb`\] Guard `hashlib` imports and mark `hashlib-digest-hex` fix as safe (`FURB181`) ([#14694](https://github.com/astral-sh/ruff/pull/14694))
### Configuration
- \[`flake8-import-conventions`\] Improve syntax check for aliases supplied in configuration for `unconventional-import-alias` (`ICN001`) ([#14745](https://github.com/astral-sh/ruff/pull/14745))
### Bug fixes
- Revert: [pyflakes] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) (#14615) ([#14726](https://github.com/astral-sh/ruff/pull/14726))
- \[`pep8-naming`\] Avoid false positive for `class Bar(type(foo))` (`N804`) ([#14683](https://github.com/astral-sh/ruff/pull/14683))
- \[`pycodestyle`\] Handle f-strings properly for `invalid-escape-sequence` (`W605`) ([#14748](https://github.com/astral-sh/ruff/pull/14748))
- \[`pylint`\] Ignore `@overload` in `PLR0904` ([#14730](https://github.com/astral-sh/ruff/pull/14730))
- \[`refurb`\] Handle non-finite decimals in `verbose-decimal-constructor` (`FURB157`) ([#14596](https://github.com/astral-sh/ruff/pull/14596))
- \[`ruff`\] Avoid emitting `assignment-in-assert` when all references to the assigned variable are themselves inside `assert`s (`RUF018`) ([#14661](https://github.com/astral-sh/ruff/pull/14661))
### Documentation
- Improve docs for `flake8-use-pathlib` rules ([#14741](https://github.com/astral-sh/ruff/pull/14741))
- Improve error messages and docs for `flake8-comprehensions` rules ([#14729](https://github.com/astral-sh/ruff/pull/14729))
- \[`flake8-type-checking`\] Expands `TC006` docs to better explain itself ([#14749](https://github.com/astral-sh/ruff/pull/14749))
## 0.8.1
### Preview features

89
Cargo.lock generated
View File

@@ -117,9 +117,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.93"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "append-only-vec"
@@ -276,13 +276,13 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.95"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
dependencies = [
"jobserver",
"libc",
"once_cell",
"shlex",
]
[[package]]
@@ -353,9 +353,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.21"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
dependencies = [
"clap_builder",
"clap_derive",
@@ -363,9 +363,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.21"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
dependencies = [
"anstream",
"anstyle",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.0"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clearscreen"
@@ -758,18 +758,18 @@ dependencies = [
[[package]]
name = "dir-test"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b12781621d53fd9087021f5a338df5c57c04f84a6231c1f4726f45e2e333470b"
checksum = "62c013fe825864f3e4593f36426c1fa7a74f5603f13ca8d1af7a990c1cd94a79"
dependencies = [
"dir-test-macros",
]
[[package]]
name = "dir-test-macros"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1340852f50b2285d01a7f598cc5d08b572669c3e09e614925175cc3c26787b91"
checksum = "d42f54d7b4a6bc2400fe5b338e35d1a335787585375322f49c5d5fe7b243da7e"
dependencies = [
"glob",
"proc-macro2",
@@ -1944,10 +1944,11 @@ checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
[[package]]
name = "pep440_rs"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0922a442c78611fa8c5ed6065d2d898a820cf12fa90604217fdb2d01675efec7"
checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c"
dependencies = [
"once_cell",
"serde",
"unicode-width 0.2.0",
"unscanny",
@@ -2160,7 +2161,7 @@ dependencies = [
"newtype-uuid",
"quick-xml",
"strip-ansi-escapes",
"thiserror 2.0.3",
"thiserror 2.0.6",
"uuid",
]
@@ -2299,6 +2300,7 @@ dependencies = [
"red_knot_vendored",
"ruff_db",
"ruff_index",
"ruff_macros",
"ruff_python_ast",
"ruff_python_literal",
"ruff_python_parser",
@@ -2312,7 +2314,7 @@ dependencies = [
"static_assertions",
"tempfile",
"test-case",
"thiserror 2.0.3",
"thiserror 2.0.6",
"tracing",
]
@@ -2358,7 +2360,9 @@ dependencies = [
"ruff_text_size",
"rustc-hash 2.1.0",
"salsa",
"serde",
"smallvec",
"toml",
]
[[package]]
@@ -2407,7 +2411,7 @@ dependencies = [
"rustc-hash 2.1.0",
"salsa",
"serde",
"thiserror 2.0.3",
"thiserror 2.0.6",
"toml",
"tracing",
]
@@ -2513,7 +2517,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.8.1"
version = "0.8.2"
dependencies = [
"anyhow",
"argfile",
@@ -2560,7 +2564,7 @@ dependencies = [
"strum",
"tempfile",
"test-case",
"thiserror 2.0.3",
"thiserror 2.0.6",
"tikv-jemallocator",
"toml",
"tracing",
@@ -2630,7 +2634,7 @@ dependencies = [
"salsa",
"serde",
"tempfile",
"thiserror 2.0.3",
"thiserror 2.0.6",
"tracing",
"tracing-subscriber",
"tracing-tree",
@@ -2732,7 +2736,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.8.1"
version = "0.8.2"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2782,7 +2786,7 @@ dependencies = [
"strum",
"strum_macros",
"test-case",
"thiserror 2.0.3",
"thiserror 2.0.6",
"toml",
"typed-arena",
"unicode-normalization",
@@ -2816,7 +2820,7 @@ dependencies = [
"serde_json",
"serde_with",
"test-case",
"thiserror 2.0.3",
"thiserror 2.0.6",
"uuid",
]
@@ -2888,7 +2892,7 @@ dependencies = [
"similar",
"smallvec",
"static_assertions",
"thiserror 2.0.3",
"thiserror 2.0.6",
"tracing",
]
@@ -3021,7 +3025,7 @@ dependencies = [
"serde",
"serde_json",
"shellexpand",
"thiserror 2.0.3",
"thiserror 2.0.6",
"tracing",
"tracing-subscriber",
]
@@ -3047,7 +3051,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.8.1"
version = "0.8.2"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3409,6 +3413,12 @@ dependencies = [
"dirs 5.0.1",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "similar"
version = "2.6.0"
@@ -3613,11 +3623,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.3"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
dependencies = [
"thiserror-impl 2.0.3",
"thiserror-impl 2.0.6",
]
[[package]]
@@ -3633,9 +3643,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.3"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
dependencies = [
"proc-macro2",
"quote",
@@ -3787,9 +3797,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
version = "0.3.6"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069580424efe11d97c3fef4197fa98c004fa26672cc71ad8770d224e23b1951d"
checksum = "74ba258e9de86447f75edf6455fded8e5242704c6fccffe7bf8d7fb6daef1180"
dependencies = [
"indicatif",
"tracing",
@@ -3961,21 +3971,18 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.11.0"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b30e6f97efe1fa43535ee241ee76967d3ff6ff3953ebb430d8d55c5393029e7b"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64 0.22.0",
"flate2",
"litemap",
"log",
"once_cell",
"rustls",
"rustls-pki-types",
"url",
"webpki-roots",
"yoke",
"zerofrom",
]
[[package]]

View File

@@ -119,6 +119,10 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
```shell
# With uv.
uv add --dev ruff # to add ruff to your project
uv tool install ruff # to install ruff globally
# With pip.
pip install ruff
@@ -136,8 +140,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.8.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.8.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.2/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -170,7 +174,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.8.1
rev: v0.8.2
hooks:
# Run the linter.
- id: ruff

View File

@@ -13,6 +13,7 @@ license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_index = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true }

View File

@@ -0,0 +1,75 @@
# Any
## Annotation
`typing.Any` is a way to name the Any type.
```py
from typing import Any
x: Any = 1
x = "foo"
def f():
reveal_type(x) # revealed: Any
```
## Aliased to a different name
If you alias `typing.Any` to another name, we still recognize that as a spelling of the Any type.
```py
from typing import Any as RenamedAny
x: RenamedAny = 1
x = "foo"
def f():
reveal_type(x) # revealed: Any
```
## Shadowed class
If you define your own class named `Any`, using that in a type expression refers to your class, and
isn't a spelling of the Any type.
```py
class Any:
pass
x: Any
def f():
reveal_type(x) # revealed: Any
# This verifies that we're not accidentally seeing typing.Any, since str is assignable
# to that but not to our locally defined class.
y: Any = "not an Any" # error: [invalid-assignment]
```
## Subclass
The spec allows you to define subclasses of `Any`.
TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The
assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment
to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore
assignable to `int`.
```py
from typing import Any
class Subclass(Any):
pass
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
x: Subclass = 1 # error: [invalid-assignment]
# TODO: no diagnostic
y: int = Subclass() # error: [invalid-assignment]
def f() -> Subclass:
pass
reveal_type(f()) # revealed: Subclass
```

View File

@@ -12,7 +12,7 @@ Parts of the testcases defined here were adapted from [the specification's examp
It can be used anywhere a type is accepted:
```py
from typing import LiteralString
from typing_extensions import LiteralString
x: LiteralString
@@ -25,7 +25,7 @@ def f():
`LiteralString` cannot be used within `Literal`:
```py
from typing import Literal, LiteralString
from typing_extensions import Literal, LiteralString
bad_union: Literal["hello", LiteralString] # error: [invalid-literal-parameter]
bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
@@ -36,7 +36,7 @@ bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
`LiteralString` cannot be parametrized.
```py
from typing import LiteralString
from typing_extensions import LiteralString
a: LiteralString[str] # error: [invalid-type-parameter]
b: LiteralString["foo"] # error: [invalid-type-parameter]
@@ -47,7 +47,7 @@ b: LiteralString["foo"] # error: [invalid-type-parameter]
Subclassing `LiteralString` leads to a runtime error.
```py
from typing import LiteralString
from typing_extensions import LiteralString
class C(LiteralString): ... # error: [invalid-base]
```
@@ -57,6 +57,8 @@ class C(LiteralString): ... # error: [invalid-base]
### Common operations
```py
from typing_extensions import LiteralString
foo: LiteralString = "foo"
reveal_type(foo) # revealed: Literal["foo"]
@@ -85,6 +87,8 @@ reveal_type(template.format(foo, bar)) # revealed: @Todo(call todo)
vice versa.
```py
from typing_extensions import Literal, LiteralString
def coinflip() -> bool:
return True
@@ -112,6 +116,8 @@ qux_3: LiteralString = baz_3 # error: [invalid-assignment]
### Narrowing
```py
from typing_extensions import LiteralString
lorem: LiteralString = "lorem" * 1_000_000_000
reveal_type(lorem) # revealed: LiteralString
@@ -125,4 +131,22 @@ if "" < lorem == "ipsum":
reveal_type(lorem) # revealed: Literal["ipsum"]
```
## `typing.LiteralString`
`typing.LiteralString` is only available in Python 3.11 and later:
```toml
[environment]
target-version = "3.11"
```
```py
from typing import LiteralString
x: LiteralString = "foo"
def f():
reveal_type(x) # revealed: LiteralString
```
[1]: https://typing.readthedocs.io/en/latest/spec/literal.html#literalstring

View File

@@ -19,12 +19,11 @@ reveal_type(stop())
## Assignment
```py
from typing import NoReturn, Never, Any
from typing_extensions import NoReturn, Never, Any
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
x: Never[int]
a1: NoReturn
# TODO: Test `Never` is only available in python >= 3.11
a2: Never
b1: Any
b2: int
@@ -46,17 +45,20 @@ def f():
v6: Never = 1
```
## Typing Extensions
## `typing.Never`
`typing.Never` is only available in Python 3.11 and later:
```toml
[environment]
target-version = "3.11"
```
```py
from typing_extensions import NoReturn, Never
from typing import Never
x: NoReturn
y: Never
x: Never
def f():
# revealed: Never
reveal_type(x)
# revealed: Never
reveal_type(y)
reveal_type(x) # revealed: Never
```

View File

@@ -8,8 +8,8 @@ from typing_extensions import TypeVarTuple
Ts = TypeVarTuple("Ts")
def append_int(*args: *Ts) -> tuple[*Ts, int]:
# TODO: should show some representation of the variadic generic type
reveal_type(args) # revealed: @Todo(function parameter type)
# TODO: tuple[*Ts]
reveal_type(args) # revealed: tuple
return (*args, 1)

View File

@@ -95,37 +95,37 @@ reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
## Various string kinds
```py
# error: [annotation-raw-string] "Type expressions cannot use raw string literal"
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
def f1() -> r"int":
return 1
# error: [annotation-f-string] "Type expressions cannot use f-strings"
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
def f2() -> f"int":
return 1
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
def f3() -> b"int":
return 1
def f4() -> "int":
return 1
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals"
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
def f5() -> "in" "t":
return 1
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
def f6() -> "\N{LATIN SMALL LETTER I}nt":
return 1
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
def f7() -> "\x69nt":
return 1
def f8() -> """int""":
return 1
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
def f9() -> "b'int'":
return 1
@@ -208,9 +208,9 @@ i: "{i for i in range(5)}"
j: "{i: i for i in range(5)}"
k: "(i for i in range(5))"
l: "await 1"
# error: [forward-annotation-syntax-error]
# error: [invalid-syntax-in-forward-annotation]
m: "yield 1"
# error: [forward-annotation-syntax-error]
# error: [invalid-syntax-in-forward-annotation]
n: "yield from 1"
o: "1 < 2"
p: "call()"

View File

@@ -50,7 +50,7 @@ reveal_type(b) # revealed: tuple[int]
reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
# TODO: homogenous tuples, PEP-646 tuples
# TODO: homogeneous tuples, PEP-646 tuples
reveal_type(e) # revealed: @Todo(full tuple[...] support)
reveal_type(f) # revealed: @Todo(full tuple[...] support)
reveal_type(g) # revealed: @Todo(full tuple[...] support)

View File

@@ -18,43 +18,3 @@ Note: in this particular example, one could argue that the most likely error wou
of the `x`/`foo` definitions, and so it could be desirable to infer `Literal[1]` for the type of
`x`. On the other hand, there might be a variable `fob` a little higher up in this file, and the
actual error might have been just a typo. Inferring `Unknown` thus seems like the safest option.
## Unbound class variable
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
class C:
y = x
if flag:
x = 2
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[2]
reveal_type(C.y) # revealed: Literal[1]
```
## Possibly unbound in class and global scope
```py
def bool_instance() -> bool:
return True
if bool_instance():
x = "abc"
class C:
if bool_instance():
x = 1
# error: [possibly-unresolved-reference]
y = x
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
```

View File

@@ -0,0 +1,219 @@
# Length (`len()`)
## Literal and constructed iterables
### Strings and bytes literals
```py
reveal_type(len("no\rmal")) # revealed: Literal[6]
reveal_type(len(r"aw stri\ng")) # revealed: Literal[10]
reveal_type(len(r"conca\t" "ena\tion")) # revealed: Literal[14]
reveal_type(len(b"ytes lite" rb"al")) # revealed: Literal[11]
reveal_type(len("𝒰𝕹🄸©🕲𝕕ℇ")) # revealed: Literal[7]
reveal_type( # revealed: Literal[7]
len(
"""foo
bar"""
)
)
reveal_type( # revealed: Literal[9]
len(
r"""foo\r
bar"""
)
)
reveal_type( # revealed: Literal[7]
len(
b"""foo
bar"""
)
)
reveal_type( # revealed: Literal[9]
len(
rb"""foo\r
bar"""
)
)
```
### Tuples
```py
reveal_type(len(())) # revealed: Literal[0]
reveal_type(len((1,))) # revealed: Literal[1]
reveal_type(len((1, 2))) # revealed: Literal[2]
# TODO: Handle constructor calls
reveal_type(len(tuple())) # revealed: int
# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[],))) # revealed: Literal[1]
# TODO: Handle star unpacks; Should be: Literal[1]
reveal_type( # revealed: Literal[2]
len(
(
*[],
1,
)
)
)
# TODO: Handle star unpacks; Should be: Literal[2]
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[], *{}))) # revealed: Literal[2]
```
### Lists, sets and dictionaries
```py
reveal_type(len([])) # revealed: int
reveal_type(len([1])) # revealed: int
reveal_type(len([1, 2])) # revealed: int
reveal_type(len([*{}, *dict()])) # revealed: int
reveal_type(len({})) # revealed: int
reveal_type(len({**{}})) # revealed: int
reveal_type(len({**{}, **{}})) # revealed: int
reveal_type(len({1})) # revealed: int
reveal_type(len({1, 2})) # revealed: int
reveal_type(len({*[], 2})) # revealed: int
reveal_type(len(list())) # revealed: int
reveal_type(len(set())) # revealed: int
reveal_type(len(dict())) # revealed: int
reveal_type(len(frozenset())) # revealed: int
```
## `__len__`
The returned value of `__len__` is implicitly and recursively converted to `int`.
### Literal integers
```py
from typing import Literal
class Zero:
def __len__(self) -> Literal[0]: ...
class ZeroOrOne:
def __len__(self) -> Literal[0, 1]: ...
class ZeroOrTrue:
def __len__(self) -> Literal[0, True]: ...
class OneOrFalse:
def __len__(self) -> Literal[1] | Literal[False]: ...
class OneOrFoo:
def __len__(self) -> Literal[1, "foo"]: ...
class ZeroOrStr:
def __len__(self) -> Literal[0] | str: ...
reveal_type(len(Zero())) # revealed: Literal[0]
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
# TODO: Emit a diagnostic
reveal_type(len(OneOrFoo())) # revealed: int
# TODO: Emit a diagnostic
reveal_type(len(ZeroOrStr())) # revealed: int
```
### Literal booleans
```py
from typing import Literal
class LiteralTrue:
def __len__(self) -> Literal[True]: ...
class LiteralFalse:
def __len__(self) -> Literal[False]: ...
reveal_type(len(LiteralTrue())) # revealed: Literal[1]
reveal_type(len(LiteralFalse())) # revealed: Literal[0]
```
### Enums
```py
from enum import Enum, auto
from typing import Literal
class SomeEnum(Enum):
AUTO = auto()
INT = 2
STR = "4"
TUPLE = (8, "16")
INT_2 = 3_2
class Auto:
def __len__(self) -> Literal[SomeEnum.AUTO]: ...
class Int:
def __len__(self) -> Literal[SomeEnum.INT]: ...
class Str:
def __len__(self) -> Literal[SomeEnum.STR]: ...
class Tuple:
def __len__(self) -> Literal[SomeEnum.TUPLE]: ...
class IntUnion:
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
reveal_type(len(Auto())) # revealed: int
reveal_type(len(Int())) # revealed: Literal[2]
reveal_type(len(Str())) # revealed: int
reveal_type(len(Tuple())) # revealed: int
reveal_type(len(IntUnion())) # revealed: Literal[2, 32]
```
### Negative integers
```py
from typing import Literal
class Negative:
def __len__(self) -> Literal[-1]: ...
# TODO: Emit a diagnostic
reveal_type(len(Negative())) # revealed: int
```
### Wrong signature
```py
from typing import Literal
class SecondOptionalArgument:
def __len__(self, v: int = 0) -> Literal[0]: ...
class SecondRequiredArgument:
def __len__(self, v: int) -> Literal[1]: ...
# TODO: Emit a diagnostic
reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]
# TODO: Emit a diagnostic
reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
```
### No `__len__`
```py
class NoDunderLen:
pass
# TODO: Emit a diagnostic
reveal_type(len(NoDunderLen())) # revealed: int
```

View File

@@ -0,0 +1,75 @@
# Function parameter types
Within a function scope, the declared type of each parameter is its annotated type (or Unknown if
not annotated). The initial inferred type is the union of the declared type with the type of the
default value expression (if any). If both are fully static types, this union should simplify to the
annotated type (since the default value type must be assignable to the annotated type, and for fully
static types this means subtype-of, which simplifies in unions). But if the annotated type is
Unknown or another non-fully-static type, the default value type may still be relevant as lower
bound.
The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is
a dictionary from strings to its annotated type.
## Parameter kinds
```py
from typing import Literal
def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, h: Literal[6] = 6, **kwargs: str):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: int
reveal_type(c) # revealed: Unknown | Literal[1]
reveal_type(d) # revealed: int
reveal_type(e) # revealed: Unknown | Literal[3]
reveal_type(f) # revealed: Literal[4]
reveal_type(g) # revealed: Unknown | Literal[5]
reveal_type(h) # revealed: Literal[6]
# TODO: should be `tuple[object, ...]` (needs generics)
reveal_type(args) # revealed: tuple
# TODO: should be `dict[str, str]` (needs generics)
reveal_type(kwargs) # revealed: dict
```
## Unannotated variadic parameters
...are inferred as tuple of Unknown or dict from string to Unknown.
```py
def g(*args, **kwargs):
# TODO: should be `tuple[Unknown, ...]` (needs generics)
reveal_type(args) # revealed: tuple
# TODO: should be `dict[str, Unknown]` (needs generics)
reveal_type(kwargs) # revealed: dict
```
## Annotation is present but not a fully static type
The default value type should be a lower bound on the inferred type.
```py
from typing import Any
def f(x: Any = 1):
reveal_type(x) # revealed: Any | Literal[1]
```
## Default value type must be assignable to annotated type
The default value type must be assignable to the annotated type. If not, we emit a diagnostic, and
fall back to inferring the annotated type, ignoring the default value type.
```py
# error: [invalid-parameter-default]
def f(x: int = "foo"):
reveal_type(x) # revealed: int
# The check is assignable-to, not subtype-of, so this is fine:
from typing import Any
def g(x: Any = "foo"):
reveal_type(x) # revealed: Any | Literal["foo"]
```

View File

@@ -73,7 +73,7 @@ def f[T]():
A typevar with less than two constraints emits a diagnostic:
```py
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
def f[T: (int,)]():
pass
```

View File

@@ -55,3 +55,24 @@ from b import x
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
```
## Import cycle
```py path=a.py
class A: ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]]
import b
class C(b.B): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
```
```py path=b.py
from a import A
class B(A): ...
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
```

View File

@@ -0,0 +1,9 @@
# Invalid syntax
## Missing module name
```py
from import bar # error: [invalid-syntax]
reveal_type(bar) # revealed: Unknown
```

View File

@@ -0,0 +1,93 @@
# Syntax errors
Test cases to ensure that red knot does not panic if there are syntax errors in the source code.
The parser cannot recover from certain syntax errors completely which is why the number of syntax
errors could be more than expected in the following examples. For instance, if there's a keyword
(like `for`) in the middle of another statement (like function definition), then it's more likely
that the rest of the tokens are going to be part of the `for` statement and not the function
definition. But, it's not necessary that the remaining tokens are valid in the context of a `for`
statement.
## Keyword as identifiers
When keywords are used as identifiers, the parser recovers from this syntax error by emitting an
error and including the text value of the keyword to create the `Identifier` node.
### Name expression
#### Assignment
```py
# error: [invalid-syntax]
pass = 1
```
#### Type alias
```py
# error: [invalid-syntax]
# error: [invalid-syntax]
type pass = 1
```
#### Function definition
```py
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
def True(for):
# error: [invalid-syntax]
# error: [invalid-syntax]
pass
```
#### For
```py
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [unresolved-reference] "Name `pass` used when not defined"
for while in pass:
pass
```
#### While
```py
# error: [invalid-syntax]
# error: [unresolved-reference] "Name `in` used when not defined"
while in:
pass
```
#### Match
```py
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [unresolved-reference] "Name `match` used when not defined"
match while:
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [unresolved-reference] "Name `case` used when not defined"
case in:
# error: [invalid-syntax]
# error: [invalid-syntax]
pass
```
### Attribute expression
```py
# TODO: Check when support for attribute expressions is added
# error: [invalid-syntax]
# error: [unresolved-reference] "Name `foo` used when not defined"
for x in foo.pass:
pass
```

View File

@@ -0,0 +1,7 @@
# Ellipsis literals
## Simple
```py
reveal_type(...) # revealed: EllipsisType | ellipsis
```

View File

@@ -0,0 +1,67 @@
This test makes sure that `red_knot_test` correctly parses the TOML configuration blocks and applies
the correct settings hierarchically.
The following configuration will be attached to the *root* section (without any heading):
```toml
[environment]
target-version = "3.10"
```
# Basic
Here, we simply make sure that we pick up the global configuration from the root section:
```py
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
```
# Inheritance
## Child
### Grandchild
The same should work for arbitrarily nested sections:
```py
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
```
# Overwriting
Here, we make sure that we can overwrite the global configuration in a child section:
```toml
[environment]
target-version = "3.11"
```
```py
reveal_type(sys.version_info[:2] == (3, 11)) # revealed: Literal[True]
```
# No global state
There is no global state. This section should again use the root configuration:
```py
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
```
# Overwriting affects children
Children in this section should all use the section configuration:
```toml
[environment]
target-version = "3.12"
```
## Child
### Grandchild
```py
reveal_type(sys.version_info[:2] == (3, 12)) # revealed: Literal[True]
```

View File

@@ -179,9 +179,9 @@ reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
```py path=a.pyi
class A(B): ... # error: [cyclic-class-def]
class B(C): ... # error: [cyclic-class-def]
class C(A): ... # error: [cyclic-class-def]
class A(B): ... # error: [cyclic-class-definition]
class B(C): ... # error: [cyclic-class-definition]
class C(A): ... # error: [cyclic-class-definition]
reveal_type(A.__class__) # revealed: Unknown
```

View File

@@ -348,14 +348,14 @@ 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
class Foo(Foo): ... # error: [cyclic-class-def]
class Foo(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo) # revealed: Literal[Foo]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Bar: ...
class Baz: ...
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def]
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
reveal_type(Boz) # revealed: Literal[Boz]
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
@@ -366,9 +366,9 @@ 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
class Foo(Bar): ... # error: [cyclic-class-def]
class Bar(Baz): ... # error: [cyclic-class-def]
class Baz(Foo): ... # error: [cyclic-class-def]
class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
@@ -379,9 +379,9 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
```py path=a.pyi
class Spam: ...
class Foo(Bar): ... # error: [cyclic-class-def]
class Bar(Baz): ... # error: [cyclic-class-def]
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
@@ -391,16 +391,16 @@ 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
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
class Bar(Foo): ...
# TODO: can we avoid emitting the errors for these?
# The classes have cyclic superclasses,
# but are not themselves cyclic...
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
class Spam(Baz): ... # error: [cyclic-class-def]
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
class Spam(Baz): ... # error: [cyclic-class-definition]
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]

View File

@@ -0,0 +1,45 @@
# Nonlocal references
## One level up
```py
def f():
x = 1
def g():
reveal_type(x) # revealed: Literal[1]
```
## Two levels up
```py
def f():
x = 1
def g():
def h():
reveal_type(x) # revealed: Literal[1]
```
## Skips class scope
```py
def f():
x = 1
class C:
x = 2
def g():
reveal_type(x) # revealed: Literal[1]
```
## Skips annotation-only assignment
```py
def f():
x = 1
def g():
# it's pretty weird to have an annotated assignment in a function where the
# name is otherwise not defined; maybe should be an error?
x: int
def h():
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -0,0 +1,57 @@
# Unbound
## Unbound class variable
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
class C:
y = x
if flag:
x = 2
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[2]
reveal_type(C.y) # revealed: Literal[1]
```
## Possibly unbound in class and global scope
```py
def bool_instance() -> bool:
return True
if bool_instance():
x = "abc"
class C:
if bool_instance():
x = 1
# error: [possibly-unresolved-reference]
y = x
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
```
## Unbound function local
An unbound function local that has definitions in the scope does not fall back to globals.
```py
x = 1
def f():
# error: [unresolved-reference]
# revealed: Unknown
reveal_type(x)
x = 2
# revealed: Literal[2]
reveal_type(x)
```

View File

@@ -1,4 +1,4 @@
# Class defenitions in stubs
# Class definitions in stubs
## Cyclical class definition

View File

@@ -1,5 +1,10 @@
# `sys.version_info`
```toml
[environment]
target-version = "3.9"
```
## The type of `sys.version_info`
The type of `sys.version_info` is `sys._version_info`, at least according to typeshed's stubs (which

View File

@@ -0,0 +1,117 @@
# type special form
## Class literal
```py
class A: ...
def f() -> type[A]:
return A
reveal_type(f()) # revealed: type[A]
```
## Nested class literal
```py
class A:
class B: ...
def f() -> type[A.B]:
return A.B
reveal_type(f()) # revealed: type[B]
```
## Deeply nested class literal
```py
class A:
class B:
class C: ...
def f() -> type[A.B.C]:
return A.B.C
reveal_type(f()) # revealed: type[C]
```
## Class literal from another module
```py
from a import A
def f() -> type[A]:
return A
reveal_type(f()) # revealed: type[A]
```
```py path=a.py
class A: ...
```
## Qualified class literal from another module
```py
import a
def f() -> type[a.B]:
return a.B
reveal_type(f()) # revealed: type[B]
```
```py path=a.py
class B: ...
```
## Deeply qualified class literal from another module
```py path=a/test.py
import a.b
# TODO: no diagnostic
# error: [unresolved-attribute]
def f() -> type[a.b.C]:
# TODO: no diagnostic
# error: [unresolved-attribute]
return a.b.C
reveal_type(f()) # revealed: @Todo(unsupported type[X] special form)
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
## Union of classes
```py
class BasicUser: ...
class ProUser: ...
class A:
class B:
class C: ...
def get_user() -> type[BasicUser | ProUser | A.B.C]:
return BasicUser
# revealed: type[BasicUser] | type[ProUser] | type[C]
reveal_type(get_user())
```
## Illegal parameters
```py
class A: ...
class B: ...
# error: [invalid-type-form]
def get_user() -> type[A, B]:
return A
```

View File

@@ -267,3 +267,42 @@ reveal_type(b) # revealed: LiteralString
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(c) # revealed: @Todo(starred unpacking)
```
### Unicode
```py
# TODO: Add diagnostic (need more values to unpack)
(a, b) = "é"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: Unknown
```
### Unicode escape (1)
```py
# TODO: Add diagnostic (need more values to unpack)
(a, b) = "\u9E6C"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: Unknown
```
### Unicode escape (2)
```py
# TODO: Add diagnostic (need more values to unpack)
(a, b) = "\U0010FFFF"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: Unknown
```
### Surrogates
```py
(a, b) = "\uD800\uDFFF"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
```

View File

@@ -1,3 +1,4 @@
use crate::lint::RuleSelection;
use ruff_db::files::File;
use ruff_db::{Db as SourceDb, Upcast};
@@ -5,18 +6,25 @@ use ruff_db::{Db as SourceDb, Upcast};
#[salsa::db]
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
fn is_file_open(&self, file: File) -> bool;
fn rule_selection(&self) -> &RuleSelection;
}
#[cfg(test)]
pub(crate) mod tests {
use std::sync::Arc;
use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast};
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::{default_lint_registry, ProgramSettings};
use super::Db;
use crate::lint::RuleSelection;
use anyhow::Context;
use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast};
#[salsa::db]
pub(crate) struct TestDb {
@@ -24,7 +32,8 @@ pub(crate) mod tests {
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
rule_selection: Arc<RuleSelection>,
}
impl TestDb {
@@ -33,8 +42,9 @@ pub(crate) mod tests {
storage: salsa::Storage::default(),
system: TestSystem::default(),
vendored: red_knot_vendored::file_system().clone(),
events: std::sync::Arc::default(),
events: Arc::default(),
files: Files::default(),
rule_selection: Arc::new(RuleSelection::from_registry(&default_lint_registry())),
}
}
@@ -97,6 +107,10 @@ pub(crate) mod tests {
fn is_file_open(&self, file: File) -> bool {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
}
#[salsa::db]
@@ -108,4 +122,66 @@ pub(crate) mod tests {
events.push(event);
}
}
pub(crate) struct TestDbBuilder<'a> {
/// Target Python version
python_version: PythonVersion,
/// Path to a custom typeshed directory
custom_typeshed: Option<SystemPathBuf>,
/// Path and content pairs for files that should be present
files: Vec<(&'a str, &'a str)>,
}
impl<'a> TestDbBuilder<'a> {
pub(crate) fn new() -> Self {
Self {
python_version: PythonVersion::default(),
custom_typeshed: None,
files: vec![],
}
}
pub(crate) fn with_python_version(mut self, version: PythonVersion) -> Self {
self.python_version = version;
self
}
pub(crate) fn with_custom_typeshed(mut self, path: &str) -> Self {
self.custom_typeshed = Some(SystemPathBuf::from(path));
self
}
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
self.files.push((path, content));
self
}
pub(crate) fn build(self) -> anyhow::Result<TestDb> {
let mut db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
db.memory_file_system().create_directory_all(&src_root)?;
db.write_files(self.files)
.context("Failed to write test files")?;
let mut search_paths = SearchPathSettings::new(src_root);
search_paths.custom_typeshed = self.custom_typeshed;
Program::from_settings(
&db,
&ProgramSettings {
target_version: self.python_version,
search_paths,
},
)
.context("Failed to configure Program settings")?;
Ok(db)
}
}
pub(crate) fn setup_db() -> TestDb {
TestDbBuilder::new().build().expect("valid TestDb setup")
}
}

View File

@@ -2,6 +2,7 @@ use std::hash::BuildHasherDefault;
use rustc_hash::FxHasher;
use crate::lint::{LintRegistry, LintRegistryBuilder};
pub use db::Db;
pub use module_name::ModuleName;
pub use module_resolver::{resolve_module, system_module_search_paths, Module};
@@ -11,6 +12,7 @@ pub use semantic_model::{HasTy, SemanticModel};
pub mod ast_node_ref;
mod db;
pub mod lint;
mod module_name;
mod module_resolver;
mod node_key;
@@ -26,3 +28,13 @@ mod unpack;
mod util;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
pub fn default_lint_registry() -> LintRegistry {
let mut registry = LintRegistryBuilder::default();
register_semantic_lints(&mut registry);
registry.build()
}
pub fn register_semantic_lints(registry: &mut LintRegistryBuilder) {
types::register_type_lints(registry);
}

View File

@@ -0,0 +1,419 @@
use itertools::Itertools;
use ruff_db::diagnostic::{LintName, Severity};
use rustc_hash::FxHashMap;
use std::hash::Hasher;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct LintMetadata {
/// The unique identifier for the lint.
pub name: LintName,
/// A one-sentence summary of what the lint catches.
pub summary: &'static str,
/// An in depth explanation of the lint in markdown. Covers what the lint does, why it's bad and possible fixes.
///
/// The documentation may require post-processing to be rendered correctly. For example, lines
/// might have leading or trailing whitespace that should be removed.
pub raw_documentation: &'static str,
/// The default level of the lint if the user doesn't specify one.
pub default_level: Level,
pub status: LintStatus,
/// Location where this lint is declared: `file_name:line`
pub source: &'static str,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Level {
/// The lint is disabled and should not run.
Ignore,
/// The lint is enabled and diagnostic should have a warning severity.
Warn,
/// The lint is enabled and diagnostics have an error severity.
Error,
}
impl Level {
pub const fn is_error(self) -> bool {
matches!(self, Level::Error)
}
pub const fn is_warn(self) -> bool {
matches!(self, Level::Warn)
}
pub const fn is_ignore(self) -> bool {
matches!(self, Level::Ignore)
}
}
impl TryFrom<Level> for Severity {
type Error = ();
fn try_from(level: Level) -> Result<Self, ()> {
match level {
Level::Ignore => Err(()),
Level::Warn => Ok(Severity::Warning),
Level::Error => Ok(Severity::Error),
}
}
}
impl LintMetadata {
pub fn name(&self) -> LintName {
self.name
}
pub fn summary(&self) -> &str {
self.summary
}
/// Returns the documentation line by line with leading and trailing whitespace removed.
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
self.raw_documentation
.lines()
.map(|line| line.strip_prefix(' ').unwrap_or(line).trim_end())
}
/// Returns the documentation as a single string.
pub fn documentation(&self) -> String {
self.documentation_lines().join("\n")
}
pub fn default_level(&self) -> Level {
self.default_level
}
pub fn status(&self) -> &LintStatus {
&self.status
}
pub fn source(&self) -> &str {
self.source
}
}
#[doc(hidden)]
pub const fn lint_metadata_defaults() -> LintMetadata {
LintMetadata {
name: LintName::of(""),
summary: "",
raw_documentation: "",
default_level: Level::Error,
status: LintStatus::preview("0.0.0"),
source: "",
}
}
#[derive(Copy, Clone, Debug)]
pub enum LintStatus {
/// The rule has been added to the linter, but is not yet stable.
Preview {
/// When the rule was added to preview
since: &'static str,
},
/// Stable rule that was added in the version defined by `since`.
Stable { since: &'static str },
/// The rule has been deprecated [`since`] (version) and will be removed in the future.
Deprecated {
since: &'static str,
reason: &'static str,
},
/// The rule has been removed [`since`] (version) and using it will result in an error.
Removed {
since: &'static str,
reason: &'static str,
},
}
impl LintStatus {
pub const fn preview(since: &'static str) -> Self {
LintStatus::Preview { since }
}
pub const fn stable(since: &'static str) -> Self {
LintStatus::Stable { since }
}
pub const fn deprecated(since: &'static str, reason: &'static str) -> Self {
LintStatus::Deprecated { since, reason }
}
pub const fn removed(since: &'static str, reason: &'static str) -> Self {
LintStatus::Removed { since, reason }
}
pub const fn is_removed(&self) -> bool {
matches!(self, LintStatus::Removed { .. })
}
}
#[macro_export]
macro_rules! declare_lint {
(
$(#[doc = $doc:literal])+
$vis: vis static $name: ident = {
summary: $summary: literal,
status: $status: expr,
// Optional properties
$( $key:ident: $value:expr, )*
}
) => {
$( #[doc = $doc] )+
#[allow(clippy::needless_update)]
$vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
summary: $summary,
raw_documentation: concat!($($doc,)+ "\n"),
status: $status,
source: concat!(file!(), ":", line!()),
$( $key: $value, )*
..$crate::lint::lint_metadata_defaults()
};
};
}
/// A unique identifier for a lint rule.
///
/// Implements `PartialEq`, `Eq`, and `Hash` based on the `LintMetadata` pointer
/// for fast comparison and lookup.
#[derive(Debug, Clone, Copy)]
pub struct LintId {
definition: &'static LintMetadata,
}
impl LintId {
pub const fn of(definition: &'static LintMetadata) -> Self {
LintId { definition }
}
}
impl PartialEq for LintId {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.definition, other.definition)
}
}
impl Eq for LintId {}
impl std::hash::Hash for LintId {
fn hash<H: Hasher>(&self, state: &mut H) {
std::ptr::hash(self.definition, state);
}
}
impl std::ops::Deref for LintId {
type Target = LintMetadata;
fn deref(&self) -> &Self::Target {
self.definition
}
}
#[derive(Default, Debug)]
pub struct LintRegistryBuilder {
/// Registered lints that haven't been removed.
lints: Vec<LintId>,
/// Lints indexed by name, including aliases and removed rules.
by_name: FxHashMap<&'static str, LintEntry>,
}
impl LintRegistryBuilder {
#[track_caller]
pub fn register_lint(&mut self, lint: &'static LintMetadata) {
assert_eq!(
self.by_name.insert(&*lint.name, lint.into()),
None,
"duplicate lint registration for '{name}'",
name = lint.name
);
if !lint.status.is_removed() {
self.lints.push(LintId::of(lint));
}
}
#[track_caller]
pub fn register_alias(&mut self, from: LintName, to: &'static LintMetadata) {
let target = match self.by_name.get(to.name.as_str()) {
Some(LintEntry::Lint(target) | LintEntry::Removed(target)) => target,
Some(LintEntry::Alias(target)) => {
panic!(
"lint alias {from} -> {to:?} points to another alias {target:?}",
target = target.name()
)
}
None => panic!(
"lint alias {from} -> {to} points to non-registered lint",
to = to.name
),
};
assert_eq!(
self.by_name
.insert(from.as_str(), LintEntry::Alias(*target)),
None,
"duplicate lint registration for '{from}'",
);
}
pub fn build(self) -> LintRegistry {
LintRegistry {
lints: self.lints,
by_name: self.by_name,
}
}
}
#[derive(Default, Debug)]
pub struct LintRegistry {
lints: Vec<LintId>,
by_name: FxHashMap<&'static str, LintEntry>,
}
impl LintRegistry {
/// Looks up a lint by its name.
pub fn get(&self, code: &str) -> Result<LintId, GetLintError> {
match self.by_name.get(code) {
Some(LintEntry::Lint(metadata)) => Ok(*metadata),
Some(LintEntry::Alias(lint)) => {
if lint.status.is_removed() {
Err(GetLintError::Removed(lint.name()))
} else {
Ok(*lint)
}
}
Some(LintEntry::Removed(lint)) => Err(GetLintError::Removed(lint.name())),
None => Err(GetLintError::Unknown(code.to_string())),
}
}
/// Returns all registered, non-removed lints.
pub fn lints(&self) -> &[LintId] {
&self.lints
}
/// Returns an iterator over all known aliases and to their target lints.
///
/// This iterator includes aliases that point to removed lints.
pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + use<'_> {
self.by_name.iter().filter_map(|(key, value)| {
if let LintEntry::Alias(alias) = value {
Some((LintName::of(key), *alias))
} else {
None
}
})
}
/// Iterates over all removed lints.
pub fn removed(&self) -> impl Iterator<Item = LintId> + use<'_> {
self.by_name.iter().filter_map(|(_, value)| {
if let LintEntry::Removed(metadata) = value {
Some(*metadata)
} else {
None
}
})
}
}
#[derive(Error, Debug, Clone)]
pub enum GetLintError {
/// The name maps to this removed lint.
#[error("lint {0} has been removed")]
Removed(LintName),
/// No lint with the given name is known.
#[error("unknown lint {0}")]
Unknown(String),
}
#[derive(Debug, PartialEq, Eq)]
pub enum LintEntry {
/// An existing lint rule. Can be in preview, stable or deprecated.
Lint(LintId),
/// A lint rule that has been removed.
Removed(LintId),
Alias(LintId),
}
impl From<&'static LintMetadata> for LintEntry {
fn from(metadata: &'static LintMetadata) -> Self {
if metadata.status.is_removed() {
LintEntry::Removed(LintId::of(metadata))
} else {
LintEntry::Lint(LintId::of(metadata))
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RuleSelection {
/// Map with the severity for each enabled lint rule.
///
/// If a rule isn't present in this map, then it should be considered disabled.
lints: FxHashMap<LintId, Severity>,
}
impl RuleSelection {
/// Creates a new rule selection from all known lints in the registry that are enabled
/// according to their default severity.
pub fn from_registry(registry: &LintRegistry) -> Self {
let lints = registry
.lints()
.iter()
.filter_map(|lint| {
Severity::try_from(lint.default_level())
.ok()
.map(|severity| (*lint, severity))
})
.collect();
RuleSelection { lints }
}
/// Returns an iterator over all enabled lints.
pub fn enabled(&self) -> impl Iterator<Item = LintId> + use<'_> {
self.lints.keys().copied()
}
/// Returns an iterator over all enabled lints and their severity.
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + use<'_> {
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
}
/// Returns the configured severity for the lint with the given id or `None` if the lint is disabled.
pub fn severity(&self, lint: LintId) -> Option<Severity> {
self.lints.get(&lint).copied()
}
/// Enables `lint` and configures with the given `severity`.
///
/// Overrides any previous configuration for the lint.
pub fn enable(&mut self, lint: LintId, severity: Severity) {
self.lints.insert(lint, severity);
}
/// Disables `lint` if it was previously enabled.
pub fn disable(&mut self, lint: LintId) {
self.lints.remove(&lint);
}
/// Merges the enabled lints from `other` into this selection.
///
/// Lints from `other` will override any existing configuration.
pub fn merge(&mut self, other: &RuleSelection) {
self.lints.extend(other.iter());
}
}

View File

@@ -606,24 +606,11 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
let function_table = index.symbol_table(function_scope_id);
assert_eq!(
names(&function_table),
vec!["a", "b", "c", "args", "d", "kwargs"],
vec!["a", "b", "c", "d", "args", "kwargs"],
);
let use_def = index.use_def_map(function_scope_id);
for name in ["a", "b", "c", "d"] {
let binding = use_def
.first_public_binding(
function_table
.symbol_id_by_name(name)
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(
binding.kind(&db),
DefinitionKind::ParameterWithDefault(_)
));
}
for name in ["args", "kwargs"] {
let binding = use_def
.first_public_binding(
function_table
@@ -633,6 +620,28 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
}
let args_binding = use_def
.first_public_binding(
function_table
.symbol_id_by_name("args")
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(
args_binding.kind(&db),
DefinitionKind::VariadicPositionalParameter(_)
));
let kwargs_binding = use_def
.first_public_binding(
function_table
.symbol_id_by_name("kwargs")
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(
kwargs_binding.kind(&db),
DefinitionKind::VariadicKeywordParameter(_)
));
}
#[test]
@@ -654,25 +663,38 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
let lambda_table = index.symbol_table(lambda_scope_id);
assert_eq!(
names(&lambda_table),
vec!["a", "b", "c", "args", "d", "kwargs"],
vec!["a", "b", "c", "d", "args", "kwargs"],
);
let use_def = index.use_def_map(lambda_scope_id);
for name in ["a", "b", "c", "d"] {
let binding = use_def
.first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
.unwrap();
assert!(matches!(
binding.kind(&db),
DefinitionKind::ParameterWithDefault(_)
));
}
for name in ["args", "kwargs"] {
let binding = use_def
.first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
}
let args_binding = use_def
.first_public_binding(
lambda_table
.symbol_id_by_name("args")
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(
args_binding.kind(&db),
DefinitionKind::VariadicPositionalParameter(_)
));
let kwargs_binding = use_def
.first_public_binding(
lambda_table
.symbol_id_by_name("kwargs")
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(
kwargs_binding.kind(&db),
DefinitionKind::VariadicKeywordParameter(_)
));
}
/// Test case to validate that the comprehension scope is correctly identified and that the target

View File

@@ -9,7 +9,7 @@ use ruff_index::IndexVec;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
use ruff_python_ast::{AnyParameterRef, BoolOp, Expr};
use ruff_python_ast::{BoolOp, Expr};
use crate::ast_node_ref::AstNodeRef;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
@@ -479,21 +479,35 @@ impl<'db> SemanticIndexBuilder<'db> {
self.pop_scope();
}
fn declare_parameter(&mut self, parameter: AnyParameterRef<'db>) {
let symbol = self.add_symbol(parameter.name().id().clone());
fn declare_parameters(&mut self, parameters: &'db ast::Parameters) {
for parameter in parameters.iter_non_variadic_params() {
self.declare_parameter(parameter);
}
if let Some(vararg) = parameters.vararg.as_ref() {
let symbol = self.add_symbol(vararg.name.id().clone());
self.add_definition(
symbol,
DefinitionNodeRef::VariadicPositionalParameter(vararg),
);
}
if let Some(kwarg) = parameters.kwarg.as_ref() {
let symbol = self.add_symbol(kwarg.name.id().clone());
self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg));
}
}
fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) {
let symbol = self.add_symbol(parameter.parameter.name.id().clone());
let definition = self.add_definition(symbol, parameter);
if let AnyParameterRef::NonVariadic(with_default) = parameter {
// Insert a mapping from the parameter to the same definition.
// This ensures that calling `HasTy::ty` on the inner parameter returns
// a valid type (and doesn't panic)
let existing_definition = self.definitions_by_node.insert(
DefinitionNodeRef::from(AnyParameterRef::Variadic(&with_default.parameter)).key(),
definition,
);
debug_assert_eq!(existing_definition, None);
}
// Insert a mapping from the inner Parameter node to the same definition.
// This ensures that calling `HasTy::ty` on the inner parameter returns
// a valid type (and doesn't panic)
let existing_definition = self
.definitions_by_node
.insert((&parameter.parameter).into(), definition);
debug_assert_eq!(existing_definition, None);
}
pub(super) fn build(mut self) -> SemanticIndex<'db> {
@@ -556,34 +570,40 @@ where
fn visit_stmt(&mut self, stmt: &'ast ast::Stmt) {
match stmt {
ast::Stmt::FunctionDef(function_def) => {
for decorator in &function_def.decorator_list {
let ast::StmtFunctionDef {
decorator_list,
parameters,
type_params,
name,
returns,
body,
is_async: _,
range: _,
} = function_def;
for decorator in decorator_list {
self.visit_decorator(decorator);
}
self.with_type_params(
NodeWithScopeRef::FunctionTypeParameters(function_def),
function_def.type_params.as_deref(),
type_params.as_deref(),
|builder| {
builder.visit_parameters(&function_def.parameters);
if let Some(expr) = &function_def.returns {
builder.visit_annotation(expr);
builder.visit_parameters(parameters);
if let Some(returns) = returns {
builder.visit_annotation(returns);
}
builder.push_scope(NodeWithScopeRef::Function(function_def));
// Add symbols and definitions for the parameters to the function scope.
for parameter in &*function_def.parameters {
builder.declare_parameter(parameter);
}
builder.declare_parameters(parameters);
builder.visit_body(&function_def.body);
builder.visit_body(body);
builder.pop_scope()
},
);
// The default value of the parameters needs to be evaluated in the
// enclosing scope.
for default in function_def
.parameters
for default in parameters
.iter_non_variadic_params()
.filter_map(|param| param.default.as_deref())
{
@@ -592,7 +612,7 @@ where
// The symbol for the function name itself has to be evaluated
// at the end to match the runtime evaluation of parameter defaults
// and return-type annotations.
let symbol = self.add_symbol(function_def.name.id.clone());
let symbol = self.add_symbol(name.id.clone());
self.add_definition(symbol, function_def);
}
ast::Stmt::ClassDef(class) => {
@@ -1179,10 +1199,8 @@ where
self.push_scope(NodeWithScopeRef::Lambda(lambda));
// Add symbols and definitions for the parameters to the lambda scope.
if let Some(parameters) = &lambda.parameters {
for parameter in parameters {
self.declare_parameter(parameter);
}
if let Some(parameters) = lambda.parameters.as_ref() {
self.declare_parameters(parameters);
}
self.visit_expr(lambda.body.as_ref());

View File

@@ -89,7 +89,9 @@ pub(crate) enum DefinitionNodeRef<'a> {
AnnotatedAssignment(&'a ast::StmtAnnAssign),
AugmentedAssignment(&'a ast::StmtAugAssign),
Comprehension(ComprehensionDefinitionNodeRef<'a>),
Parameter(ast::AnyParameterRef<'a>),
VariadicPositionalParameter(&'a ast::Parameter),
VariadicKeywordParameter(&'a ast::Parameter),
Parameter(&'a ast::ParameterWithDefault),
WithItem(WithItemDefinitionNodeRef<'a>),
MatchPattern(MatchPatternDefinitionNodeRef<'a>),
ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>),
@@ -188,8 +190,8 @@ impl<'a> From<ComprehensionDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
}
}
impl<'a> From<ast::AnyParameterRef<'a>> for DefinitionNodeRef<'a> {
fn from(node: ast::AnyParameterRef<'a>) -> Self {
impl<'a> From<&'a ast::ParameterWithDefault> for DefinitionNodeRef<'a> {
fn from(node: &'a ast::ParameterWithDefault) -> Self {
Self::Parameter(node)
}
}
@@ -315,14 +317,15 @@ impl<'db> DefinitionNodeRef<'db> {
first,
is_async,
}),
DefinitionNodeRef::Parameter(parameter) => match parameter {
ast::AnyParameterRef::Variadic(parameter) => {
DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter))
}
ast::AnyParameterRef::NonVariadic(parameter) => {
DefinitionKind::ParameterWithDefault(AstNodeRef::new(parsed, parameter))
}
},
DefinitionNodeRef::VariadicPositionalParameter(parameter) => {
DefinitionKind::VariadicPositionalParameter(AstNodeRef::new(parsed, parameter))
}
DefinitionNodeRef::VariadicKeywordParameter(parameter) => {
DefinitionKind::VariadicKeywordParameter(AstNodeRef::new(parsed, parameter))
}
DefinitionNodeRef::Parameter(parameter) => {
DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter))
}
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef {
node,
target,
@@ -384,10 +387,9 @@ impl<'db> DefinitionNodeRef<'db> {
is_async: _,
}) => target.into(),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
Self::Parameter(node) => match node {
ast::AnyParameterRef::Variadic(parameter) => parameter.into(),
ast::AnyParameterRef::NonVariadic(parameter) => parameter.into(),
},
Self::VariadicPositionalParameter(node) => node.into(),
Self::VariadicKeywordParameter(node) => node.into(),
Self::Parameter(node) => node.into(),
Self::WithItem(WithItemDefinitionNodeRef {
node: _,
target,
@@ -452,8 +454,9 @@ pub enum DefinitionKind<'db> {
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
For(ForStmtDefinitionKind),
Comprehension(ComprehensionDefinitionKind),
Parameter(AstNodeRef<ast::Parameter>),
ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>),
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
Parameter(AstNodeRef<ast::ParameterWithDefault>),
WithItem(WithItemDefinitionKind),
MatchPattern(MatchPatternDefinitionKind),
ExceptHandler(ExceptHandlerDefinitionKind),
@@ -475,7 +478,8 @@ impl DefinitionKind<'_> {
| DefinitionKind::ParamSpec(_)
| DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding,
// a parameter always binds a value, but is only a declaration if annotated
DefinitionKind::Parameter(parameter) => {
DefinitionKind::VariadicPositionalParameter(parameter)
| DefinitionKind::VariadicKeywordParameter(parameter) => {
if parameter.annotation.is_some() {
DefinitionCategory::DeclarationAndBinding
} else {
@@ -483,7 +487,7 @@ impl DefinitionKind<'_> {
}
}
// presence of a default is irrelevant, same logic as for a no-default parameter
DefinitionKind::ParameterWithDefault(parameter_with_default) => {
DefinitionKind::Parameter(parameter_with_default) => {
if parameter_with_default.parameter.annotation.is_some() {
DefinitionCategory::DeclarationAndBinding
} else {
@@ -743,6 +747,15 @@ impl From<&ast::ParameterWithDefault> for DefinitionNodeKey {
}
}
impl From<ast::AnyParameterRef<'_>> for DefinitionNodeKey {
fn from(value: ast::AnyParameterRef) -> Self {
Self(match value {
ast::AnyParameterRef::Variadic(node) => NodeKey::from_node(node),
ast::AnyParameterRef::NonVariadic(node) => NodeKey::from_node(node),
})
}
}
impl From<&ast::Identifier> for DefinitionNodeKey {
fn from(identifier: &ast::Identifier) -> Self {
Self(NodeKey::from_node(identifier))

View File

@@ -166,31 +166,15 @@ impl_binding_has_ty!(ast::ParameterWithDefault);
mod tests {
use ruff_db::files::system_path_to_file;
use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::{HasTy, ProgramSettings, SemanticModel};
fn setup_db<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<TestDb> {
let mut db = TestDb::new();
db.write_files(files)?;
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(SystemPathBuf::from("/src")),
},
)?;
Ok(db)
}
use crate::db::tests::TestDbBuilder;
use crate::{HasTy, SemanticModel};
#[test]
fn function_ty() -> anyhow::Result<()> {
let db = setup_db([("/src/foo.py", "def test(): pass")])?;
let db = TestDbBuilder::new()
.with_file("/src/foo.py", "def test(): pass")
.build()?;
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
@@ -207,7 +191,9 @@ mod tests {
#[test]
fn class_ty() -> anyhow::Result<()> {
let db = setup_db([("/src/foo.py", "class Test: pass")])?;
let db = TestDbBuilder::new()
.with_file("/src/foo.py", "class Test: pass")
.build()?;
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
@@ -224,10 +210,10 @@ mod tests {
#[test]
fn alias_ty() -> anyhow::Result<()> {
let db = setup_db([
("/src/foo.py", "class Test: pass"),
("/src/bar.py", "from foo import Test"),
])?;
let db = TestDbBuilder::new()
.with_file("/src/foo.py", "class Test: pass")
.with_file("/src/bar.py", "from foo import Test")
.build()?;
let bar = system_path_to_file(&db, "/src/bar.py").unwrap();

View File

@@ -90,7 +90,7 @@ impl<'db> Symbol<'db> {
#[cfg(test)]
mod tests {
use super::*;
use crate::types::tests::setup_db;
use crate::db::tests::setup_db;
#[test]
fn test_symbol_or_fall_back_to() {

View File

@@ -2,11 +2,12 @@ use std::hash::Hash;
use indexmap::IndexSet;
use itertools::Itertools;
use ruff_db::diagnostic::{DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_python_ast as ast;
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub(crate) use self::diagnostic::register_type_lints;
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
@@ -25,10 +26,10 @@ use crate::stdlib::{
builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule,
};
use crate::symbol::{Boundness, Symbol};
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder;
use crate::types::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
use crate::types::mro::{ClassBase, Mro, MroError, MroIterator};
use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, Module, Program};
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
mod builder;
mod diagnostic;
@@ -225,6 +226,18 @@ fn definition_expression_ty<'db>(
}
}
/// Get the type of an expression from an arbitrary scope.
///
/// Can cause query cycles if used carelessly; caller must be sure that type inference isn't
/// currently in progress for the expression's scope.
fn expression_ty<'db>(db: &'db dyn Db, file: File, expression: &ast::Expr) -> Type<'db> {
let index = semantic_index(db, file);
let file_scope = index.expression_scope_id(expression);
let scope = file_scope.to_scope_id(db, file);
let expr_id = expression.scoped_expression_id(db, scope);
infer_scope_types(db, scope).expression_ty(expr_id)
}
/// Infer the combined type of an iterator of bindings.
///
/// Will return a union if there is more than one binding.
@@ -1027,9 +1040,24 @@ impl<'db> Type<'db> {
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::KnownInstance(_) => true,
Type::SubclassOf(subclass_of) => subclass_of.class.is_fully_static(db),
Type::ClassLiteral(class_literal) => class_literal.class.is_fully_static(db),
Type::Instance(instance) => instance.class.is_fully_static(db),
Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) => {
// TODO: Ideally, we would iterate over the MRO of the class, check if all
// bases are fully static, and only return `true` if that is the case.
//
// This does not work yet, because we currently infer `Unknown` for some
// generic base classes that we don't understand yet. For example, `str`
// is defined as `class str(Sequence[str])` in typeshed and we currently
// compute its MRO as `(str, Unknown, object)`. This would make us think
// that `str` is a gradual type, which causes all sorts of downstream
// issues because it does not participate in equivalence/subtyping etc.
//
// Another problem is that we run into problems if we eagerly query the
// MRO of class literals here. I have not fully investigated this, but
// iterating over the MRO alone, without even acting on it, causes us to
// infer `Unknown` for many classes.
true
}
Type::Union(union) => union
.elements(db)
.iter()
@@ -1402,21 +1430,76 @@ impl<'db> Type<'db> {
}
}
/// Return the type of `len()` on a type if it is known more precisely than `int`,
/// or `None` otherwise.
///
/// In the second case, the return type of `len()` in `typeshed` (`int`)
/// is used as a fallback.
fn len(&self, db: &'db dyn Db) -> Option<Type<'db>> {
fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
match ty {
// TODO: Emit diagnostic for non-integers and negative integers
Type::IntLiteral(value) => (value >= 0).then_some(ty),
Type::BooleanLiteral(value) => Some(Type::IntLiteral(value.into())),
Type::Union(union) => {
let mut builder = UnionBuilder::new(db);
for element in union.elements(db) {
builder = builder.add(non_negative_int_literal(db, *element)?);
}
Some(builder.build())
}
_ => None,
}
}
let usize_len = match self {
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
Type::StringLiteral(string) => Some(string.python_len(db)),
Type::Tuple(tuple) => Some(tuple.len(db)),
_ => None,
};
if let Some(usize_len) = usize_len {
return usize_len.try_into().ok().map(Type::IntLiteral);
}
let return_ty = match self.call_dunder(db, "__len__", &[*self]) {
// TODO: emit a diagnostic
CallDunderResult::MethodNotAvailable => return None,
CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => {
outcome.return_ty(db)?
}
};
non_negative_int_literal(db, return_ty)
}
/// Return the outcome of calling an object of this type.
#[must_use]
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> {
match self {
// TODO validate typed call arguments vs callable signature
Type::FunctionLiteral(function_type) => {
if function_type.is_known(db, KnownFunction::RevealType) {
CallOutcome::revealed(
function_type.signature(db).return_ty,
*arg_types.first().unwrap_or(&Type::Unknown),
)
} else {
CallOutcome::callable(function_type.signature(db).return_ty)
Type::FunctionLiteral(function_type) => match function_type.known(db) {
Some(KnownFunction::RevealType) => CallOutcome::revealed(
function_type.signature(db).return_ty,
*arg_types.first().unwrap_or(&Type::Unknown),
),
Some(KnownFunction::Len) => {
let normal_return_ty = function_type.signature(db).return_ty;
let [only_arg] = arg_types else {
// TODO: Emit a diagnostic
return CallOutcome::callable(normal_return_ty);
};
let len_ty = only_arg.len(db);
CallOutcome::callable(len_ty.unwrap_or(normal_return_ty))
}
}
_ => CallOutcome::callable(function_type.signature(db).return_ty),
},
// TODO annotated return type on `__new__` or metaclass `__call__`
Type::ClassLiteral(ClassLiteralType { class }) => {
@@ -1619,6 +1702,7 @@ impl<'db> Type<'db> {
Type::Never
}
Type::KnownInstance(KnownInstanceType::LiteralString) => Type::LiteralString,
Type::KnownInstance(KnownInstanceType::Any) => Type::Any,
_ => todo_type!(),
}
}
@@ -1826,13 +1910,13 @@ impl<'db> KnownClass {
}
pub fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
core_module_symbol(db, self.canonical_module(), self.as_str())
core_module_symbol(db, self.canonical_module(db), self.as_str())
.ignore_possibly_unbound()
.unwrap_or(Type::Unknown)
}
/// Return the module in which we should look up the definition for this class
pub(crate) const fn canonical_module(self) -> CoreStdlibModule {
pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule {
match self {
Self::Bool
| Self::Object
@@ -1850,10 +1934,18 @@ impl<'db> KnownClass {
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
Self::NoneType => CoreStdlibModule::Typeshed,
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing,
// TODO when we understand sys.version_info, we will need an explicit fallback here,
// because typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
// singleton, but not for `typing._NoDefaultType`
Self::NoDefaultType => CoreStdlibModule::TypingExtensions,
Self::NoDefaultType => {
let python_version = Program::get(db).target_version(db);
// typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
// singleton, but not for `typing._NoDefaultType`. So we need to switch
// to `typing._NoDefaultType` for newer versions:
if python_version >= PythonVersion::PY313 {
CoreStdlibModule::Typing
} else {
CoreStdlibModule::TypingExtensions
}
}
}
}
@@ -1913,11 +2005,11 @@ impl<'db> KnownClass {
};
let module = file_to_module(db, file)?;
candidate.check_module(&module).then_some(candidate)
candidate.check_module(db, &module).then_some(candidate)
}
/// Return `true` if the module of `self` matches `module_name`
fn check_module(self, module: &Module) -> bool {
fn check_module(self, db: &'db dyn Db, module: &Module) -> bool {
if !module.search_path().is_standard_library() {
return false;
}
@@ -1937,7 +2029,7 @@ impl<'db> KnownClass {
| Self::GenericAlias
| Self::ModuleType
| Self::VersionInfo
| Self::FunctionType => module.name() == self.canonical_module().as_str(),
| Self::FunctionType => module.name() == self.canonical_module(db).as_str(),
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
matches!(module.name().as_str(), "typing" | "typing_extensions")
@@ -1961,6 +2053,8 @@ pub enum KnownInstanceType<'db> {
NoReturn,
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
Never,
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
Any,
/// A single instance of `typing.TypeVar`
TypeVar(TypeVarInstance<'db>),
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
@@ -1978,6 +2072,7 @@ impl<'db> KnownInstanceType<'db> {
Self::TypeVar(_) => "TypeVar",
Self::NoReturn => "NoReturn",
Self::Never => "Never",
Self::Any => "Any",
Self::TypeAliasType(_) => "TypeAliasType",
}
}
@@ -1992,6 +2087,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Union
| Self::NoReturn
| Self::Never
| Self::Any
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
}
}
@@ -2005,6 +2101,7 @@ impl<'db> KnownInstanceType<'db> {
Self::Union => "typing.Union",
Self::NoReturn => "typing.NoReturn",
Self::Never => "typing.Never",
Self::Any => "typing.Any",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
}
@@ -2019,6 +2116,7 @@ impl<'db> KnownInstanceType<'db> {
Self::Union => KnownClass::SpecialForm,
Self::NoReturn => KnownClass::SpecialForm,
Self::Never => KnownClass::SpecialForm,
Self::Any => KnownClass::Object,
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
}
@@ -2038,6 +2136,7 @@ impl<'db> KnownInstanceType<'db> {
return None;
}
match (module.name().as_str(), instance_name) {
("typing", "Any") => Some(Self::Any),
("typing" | "typing_extensions", "Literal") => Some(Self::Literal),
("typing" | "typing_extensions", "LiteralString") => Some(Self::LiteralString),
("typing" | "typing_extensions", "Optional") => Some(Self::Optional),
@@ -2202,9 +2301,9 @@ impl<'db> CallOutcome<'db> {
not_callable_ty,
return_ty,
}) => {
diagnostics.add(
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
"call-non-callable",
format_args!(
"Object of type `{}` is not callable",
not_callable_ty.display(db)
@@ -2217,9 +2316,9 @@ impl<'db> CallOutcome<'db> {
called_ty,
return_ty,
}) => {
diagnostics.add(
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
"call-non-callable",
format_args!(
"Object of type `{}` is not callable (due to union element `{}`)",
called_ty.display(db),
@@ -2233,9 +2332,9 @@ impl<'db> CallOutcome<'db> {
called_ty,
return_ty,
}) => {
diagnostics.add(
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
"call-non-callable",
format_args!(
"Object of type `{}` is not callable (due to union elements {})",
called_ty.display(db),
@@ -2248,9 +2347,9 @@ impl<'db> CallOutcome<'db> {
callable_ty: called_ty,
return_ty,
}) => {
diagnostics.add(
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
"call-non-callable",
format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
called_ty.display(db)
@@ -2276,7 +2375,8 @@ impl<'db> CallOutcome<'db> {
} => {
diagnostics.add(
node,
"revealed-type",
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
);
Ok(*return_ty)
@@ -2574,13 +2674,15 @@ pub enum KnownFunction {
ConstraintFunction(KnownConstraintFunction),
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
RevealType,
/// `builtins.len`
Len,
}
impl KnownFunction {
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
match self {
Self::ConstraintFunction(f) => Some(f),
Self::RevealType => None,
Self::RevealType | Self::Len => None,
}
}
@@ -2597,6 +2699,7 @@ impl KnownFunction {
"issubclass" if definition.is_builtin_definition(db) => Some(
KnownFunction::ConstraintFunction(KnownConstraintFunction::IsSubclass),
),
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
_ => None,
}
}
@@ -2650,27 +2753,6 @@ impl<'db> Class<'db> {
.map(|ClassLiteralType { class }| class)
}
/// Returns `true` if this class only contains fully-static entries in its MRO.
fn is_fully_static(self, db: &'db dyn Db) -> bool {
matches!(
self.known(db),
Some(
// TODO: probably not complete/correct:
KnownClass::Bool
| KnownClass::Object
| KnownClass::Bytes
| KnownClass::Int
| KnownClass::Float
| KnownClass::Str
| KnownClass::Set
| KnownClass::List
| KnownClass::Dict
| KnownClass::Tuple
| KnownClass::VersionInfo
)
) || self.iter_mro(db).all(ClassBase::is_fully_static)
}
#[salsa::tracked(return_ref)]
fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
let class_stmt = self.node(db);
@@ -2727,7 +2809,11 @@ impl<'db> Class<'db> {
pub fn is_subclass_of(self, db: &'db dyn Db, other: Class) -> bool {
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
// participate, so we should not return `True` if we find `Any/Unknown` in the MRO.
self.iter_mro(db).contains(&ClassBase::Class(other))
self.is_subclass_of_base(db, other)
}
fn is_subclass_of_base(self, db: &'db dyn Db, other: impl Into<ClassBase<'db>>) -> bool {
self.iter_mro(db).contains(&other.into())
}
/// Return the explicit `metaclass` of this class, if one is defined.
@@ -3068,8 +3154,9 @@ pub struct StringLiteralType<'db> {
}
impl<'db> StringLiteralType<'db> {
pub fn len(&self, db: &'db dyn Db) -> usize {
self.value(db).len()
/// The length of the string, as would be returned by Python's `len()`.
pub fn python_len(&self, db: &'db dyn Db) -> usize {
self.value(db).chars().count()
}
}
@@ -3079,6 +3166,12 @@ pub struct BytesLiteralType<'db> {
value: Box<[u8]>,
}
impl<'db> BytesLiteralType<'db> {
pub fn python_len(&self, db: &'db dyn Db) -> usize {
self.value(db).len()
}
}
#[salsa::interned]
pub struct SliceLiteralType<'db> {
start: Option<i32>,
@@ -3132,38 +3225,16 @@ static_assertions::assert_eq_size!(Type, [u8; 16]);
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::db::tests::{setup_db, TestDb, TestDbBuilder};
use crate::stdlib::typing_symbol;
use crate::ProgramSettings;
use crate::PythonVersion;
use ruff_db::files::system_path_to_file;
use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::system::DbWithTestSystem;
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast as ast;
use test_case::test_case;
pub(crate) fn setup_db() -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(src_root),
},
)
.expect("Valid search path settings");
db
}
/// A test representation of a type that can be transformed unambiguously into a real Type,
/// given a db.
#[derive(Debug, Clone, PartialEq)]
@@ -3634,13 +3705,28 @@ pub(crate) mod tests {
#[test_case(Ty::None)]
#[test_case(Ty::BooleanLiteral(true))]
#[test_case(Ty::BooleanLiteral(false))]
#[test_case(Ty::KnownClassInstance(KnownClass::NoDefaultType))]
fn is_singleton(from: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_singleton(&db));
}
/// Explicitly test for Python version <3.13 and >=3.13, to ensure that
/// the fallback to `typing_extensions` is working correctly.
/// See [`KnownClass::canonical_module`] for more information.
#[test_case(PythonVersion::PY312)]
#[test_case(PythonVersion::PY313)]
fn no_default_type_is_singleton(python_version: PythonVersion) {
let db = TestDbBuilder::new()
.with_python_version(python_version)
.build()
.unwrap();
let no_default = Ty::KnownClassInstance(KnownClass::NoDefaultType).into_type(&db);
assert!(no_default.is_singleton(&db));
}
#[test_case(Ty::None)]
#[test_case(Ty::BooleanLiteral(true))]
#[test_case(Ty::IntLiteral(1))]
@@ -3686,17 +3772,9 @@ pub(crate) mod tests {
#[test_case(Ty::StringLiteral("abc"))]
#[test_case(Ty::LiteralString)]
#[test_case(Ty::BytesLiteral("abc"))]
#[test_case(Ty::KnownClassInstance(KnownClass::Bool))]
#[test_case(Ty::KnownClassInstance(KnownClass::Object))]
#[test_case(Ty::KnownClassInstance(KnownClass::Bytes))]
#[test_case(Ty::KnownClassInstance(KnownClass::Type))]
#[test_case(Ty::KnownClassInstance(KnownClass::Int))]
#[test_case(Ty::KnownClassInstance(KnownClass::Float))]
#[test_case(Ty::KnownClassInstance(KnownClass::Str))]
#[test_case(Ty::KnownClassInstance(KnownClass::List))]
#[test_case(Ty::KnownClassInstance(KnownClass::Tuple))]
#[test_case(Ty::KnownClassInstance(KnownClass::Set))]
#[test_case(Ty::KnownClassInstance(KnownClass::Dict))]
#[test_case(Ty::KnownClassInstance(KnownClass::Object))]
#[test_case(Ty::KnownClassInstance(KnownClass::Type))]
#[test_case(Ty::BuiltinClassLiteral("str"))]
#[test_case(Ty::TypingLiteral)]
#[test_case(Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Str), Ty::None]))]
@@ -3722,47 +3800,6 @@ pub(crate) mod tests {
assert!(!from.into_type(&db).is_fully_static(&db));
}
#[test]
fn is_fully_static_for_classes() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
r#"
# TODO: change this to `from typing import Any` once we understand that
from unknown_module import UnknownClass
class FullyStaticBase: ...
class FullyStatic(FullyStaticBase): ...
fully_static_instance = FullyStatic()
class GradualBase(UnknownClass): ...
class Gradual(GradualBase): ...
gradual_instance = Gradual()
"#,
)?;
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py")?;
let fully_static_class = super::global_symbol(&db, module, "FullyStatic").expect_type();
assert!(fully_static_class.is_class_literal());
assert!(fully_static_class.is_fully_static(&db));
let fully_static_instance =
super::global_symbol(&db, module, "fully_static_instance").expect_type();
assert!(matches!(fully_static_instance, Type::Instance(_)));
assert!(fully_static_instance.is_fully_static(&db));
let gradual_class = super::global_symbol(&db, module, "Gradual").expect_type();
assert!(!gradual_class.is_fully_static(&db));
let gradual_instance = super::global_symbol(&db, module, "gradual_instance").expect_type();
assert!(matches!(gradual_instance, Type::Instance(_)));
assert!(!gradual_instance.is_fully_static(&db));
Ok(())
}
#[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")]
#[test_case(Ty::IntLiteral(-1))]
#[test_case(Ty::StringLiteral("foo"))]
@@ -3817,7 +3854,10 @@ pub(crate) mod tests {
#[test]
fn typing_vs_typeshed_no_default() {
let db = setup_db();
let db = TestDbBuilder::new()
.with_python_version(PythonVersion::PY313)
.build()
.unwrap();
let typing_no_default = typing_symbol(&db, "NoDefault").expect_type();
let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault").expect_type();

View File

@@ -378,35 +378,14 @@ impl<'db> InnerIntersectionBuilder<'db> {
#[cfg(test)]
mod tests {
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
use crate::ProgramSettings;
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::system::DbWithTestSystem;
use test_case::test_case;
fn setup_db() -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(src_root),
},
)
.expect("Valid search path settings");
db
}
#[test]
fn build_union() {
let db = setup_db();

View File

@@ -1,6 +1,12 @@
use crate::lint::{Level, LintId, LintMetadata, LintRegistryBuilder, LintStatus};
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::{ClassLiteralType, Type};
use crate::Db;
use ruff_db::diagnostic::{Diagnostic, Severity};
use crate::{declare_lint, Db};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
@@ -9,18 +15,411 @@ use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
/// Registers all known type check lints.
pub(crate) fn register_type_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&UNRESOLVED_REFERENCE);
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
registry.register_lint(&NOT_ITERABLE);
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
registry.register_lint(&NON_SUBSCRIPTABLE);
registry.register_lint(&UNRESOLVED_IMPORT);
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
registry.register_lint(&INVALID_ASSIGNMENT);
registry.register_lint(&INVALID_DECLARATION);
registry.register_lint(&CONFLICTING_DECLARATIONS);
registry.register_lint(&DIVISION_BY_ZERO);
registry.register_lint(&CALL_NON_CALLABLE);
registry.register_lint(&INVALID_TYPE_PARAMETER);
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&CYCLIC_CLASS_DEFINITION);
registry.register_lint(&DUPLICATE_BASE);
registry.register_lint(&INVALID_BASE);
registry.register_lint(&INCONSISTENT_MRO);
registry.register_lint(&INVALID_LITERAL_PARAMETER);
registry.register_lint(&CALL_POSSIBLY_UNBOUND_METHOD);
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
registry.register_lint(&CONFLICTING_METACLASS);
registry.register_lint(&UNSUPPORTED_OPERATOR);
registry.register_lint(&INVALID_CONTEXT_MANAGER);
registry.register_lint(&UNDEFINED_REVEAL);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_TYPE_FORM);
// String annotations
registry.register_lint(&FSTRING_TYPE_ANNOTATION);
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
registry.register_lint(&RAW_STRING_TYPE_ANNOTATION);
registry.register_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION);
registry.register_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION);
registry.register_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION);
}
declare_lint! {
/// ## What it does
/// Checks for references to names that are not defined.
///
/// ## Why is this bad?
/// Using an undefined variable will raise a `NameError` at runtime.
///
/// ## Example
///
/// ```python
/// print(x) # NameError: name 'x' is not defined
/// ```
pub(crate) static UNRESOLVED_REFERENCE = {
summary: "detects references to names that are not defined",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for references to names that are possibly not defined..
///
/// ## Why is this bad?
/// Using an undefined variable will raise a `NameError` at runtime.
///
/// ## Example
///
/// ```python
/// for i in range(0):
/// x = i
///
/// print(x) # NameError: name 'x' is not defined
/// ```
pub(crate) static POSSIBLY_UNRESOLVED_REFERENCE = {
summary: "detects references to possibly unresolved references",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for objects that are not iterable but are used in a context that requires them to be.
///
/// ## Why is this bad?
/// Iterating over an object that is not iterable will raise a `TypeError` at runtime.
///
/// ## Examples
///
/// ```python
/// for i in 34: # TypeError: 'int' object is not iterable
/// pass
/// ```
pub(crate) static NOT_ITERABLE = {
summary: "detects objects that are not iterable",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// TODO
pub(crate) static INDEX_OUT_OF_BOUNDS = {
summary: "detects index out of bounds errors",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for objects that do not support subscripting but are used in a context that requires them to be.
///
/// ## Why is this bad?
/// Subscripting an object that does not support it will raise a `TypeError` at runtime.
///
/// ## Examples
/// ```python
/// 4[1] # TypeError: 'int' object is not subscriptable
/// ```
pub(crate) static NON_SUBSCRIPTABLE = {
summary: "detects objects that do not support subscripting",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for import statements for which the module cannot be resolved.
///
/// ## Why is this bad?
/// Importing a module that cannot be resolved will raise an `ImportError` at runtime.
pub(crate) static UNRESOLVED_IMPORT = {
summary: "detects unresolved imports",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static POSSIBLY_UNBOUND_IMPORT = {
summary: "detects possibly unbound imports",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for step size 0 in slices.
///
/// ## Why is this bad?
/// A slice with a step size of zero will raise a `ValueError` at runtime.
///
/// ## Examples
/// ```python
/// l = list(range(10))
/// l[1:10:0] # ValueError: slice step cannot be zero
pub(crate) static ZERO_STEPSIZE_IN_SLICE = {
summary: "detects a slice step size of zero",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_ASSIGNMENT = {
summary: "detects invalid assignments",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_DECLARATION = {
summary: "detects invalid declarations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static CONFLICTING_DECLARATIONS = {
summary: "detects conflicting declarations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// It detects division by zero.
///
/// ## Why is this bad?
/// Dividing by zero raises a `ZeroDivisionError` at runtime.
///
/// ## Examples
/// ```python
/// 5 / 0
/// ```
pub(crate) static DIVISION_BY_ZERO = {
summary: "detects division by zero",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to non-callable objects.
///
/// ## Why is this bad?
/// Calling a non-callable object will raise a `TypeError` at runtime.
///
/// ## Examples
/// ```python
/// 4() # TypeError: 'int' object is not callable
/// ```
pub(crate) static CALL_NON_CALLABLE = {
summary: "detects calls to non-callable objects",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// TODO
pub(crate) static INVALID_TYPE_PARAMETER = {
summary: "detects invalid type parameters",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_TYPE_VARIABLE_CONSTRAINTS = {
summary: "detects invalid type variable constraints",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for class definitions with a cyclic inheritance chain.
///
/// ## Why is it bad?
/// TODO
pub(crate) static CYCLIC_CLASS_DEFINITION = {
summary: "detects cyclic class definitions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static DUPLICATE_BASE = {
summary: "detects class definitions with duplicate bases",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_BASE = {
summary: "detects class definitions with an invalid base",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INCONSISTENT_MRO = {
summary: "detects class definitions with an inconsistent MRO",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for invalid parameters to `typing.Literal`.
pub(crate) static INVALID_LITERAL_PARAMETER = {
summary: "detects invalid literal parameters",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to possibly unbound methods.
pub(crate) static CALL_POSSIBLY_UNBOUND_METHOD = {
summary: "detects calls to possibly unbound methods",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for possibly unbound attributes.
pub(crate) static POSSIBLY_UNBOUND_ATTRIBUTE = {
summary: "detects references to possibly unbound attributes",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for unresolved attributes.
pub(crate) static UNRESOLVED_ATTRIBUTE = {
summary: "detects references to unresolved attributes",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static CONFLICTING_METACLASS = {
summary: "detects conflicting metaclasses",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for binary expressions, comparisons, and unary expressions where the operands don't support the operator.
pub(crate) static UNSUPPORTED_OPERATOR = {
summary: "detects binary expressions where the operands don't support the operator",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_CONTEXT_MANAGER = {
summary: "detects expressions used in with statements that don't implement the context manager protocol",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to `reveal_type` without importing it.
///
/// ## Why is this bad?
/// Using `reveal_type` without importing it will raise a `NameError` at runtime.
pub(crate) static UNDEFINED_REVEAL = {
summary: "detects usages of `reveal_type` without importing it",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for default values that can't be assigned to the parameter's annotated type.
pub(crate) static INVALID_PARAMETER_DEFAULT = {
summary: "detects default values that can't be assigned to the parameter's annotated type",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for `type[]` usages that have too many or too few type arguments.
pub(crate) static INVALID_TYPE_FORM = {
summary: "detects invalid type forms",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic {
// TODO: Don't use string keys for rules
pub(super) rule: String,
pub(super) id: DiagnosticId,
pub(super) message: String,
pub(super) range: TextRange,
pub(super) severity: Severity,
pub(super) file: File,
}
impl TypeCheckDiagnostic {
pub fn rule(&self) -> &str {
&self.rule
pub fn id(&self) -> DiagnosticId {
self.id
}
pub fn message(&self) -> &str {
@@ -33,8 +432,8 @@ impl TypeCheckDiagnostic {
}
impl Diagnostic for TypeCheckDiagnostic {
fn rule(&self) -> &str {
TypeCheckDiagnostic::rule(self)
fn id(&self) -> DiagnosticId {
self.id
}
fn message(&self) -> Cow<str> {
@@ -50,7 +449,7 @@ impl Diagnostic for TypeCheckDiagnostic {
}
fn severity(&self) -> Severity {
Severity::Error
self.severity
}
}
@@ -150,9 +549,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) {
self.add(
self.add_lint(
&NOT_ITERABLE,
node,
"not-iterable",
format_args!(
"Object of type `{}` is not iterable",
not_iterable_ty.display(self.db)
@@ -167,9 +566,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
node: AnyNodeRef,
element_ty: Type<'db>,
) {
self.add(
self.add_lint(
&NOT_ITERABLE,
node,
"not-iterable",
format_args!(
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
element_ty.display(self.db)
@@ -186,9 +585,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
length: usize,
index: i64,
) {
self.add(
self.add_lint(
&INDEX_OUT_OF_BOUNDS,
node,
"index-out-of-bounds",
format_args!(
"Index {index} is out of bounds for {kind} `{}` with length {length}",
tuple_ty.display(self.db)
@@ -203,9 +602,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
non_subscriptable_ty: Type<'db>,
method: &str,
) {
self.add(
self.add_lint(
&NON_SUBSCRIPTABLE,
node,
"non-subscriptable",
format_args!(
"Cannot subscript object of type `{}` with no `{method}` method",
non_subscriptable_ty.display(self.db)
@@ -219,9 +618,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
level: u32,
module: Option<&str>,
) {
self.add(
self.add_lint(
&UNRESOLVED_IMPORT,
import_node.into(),
"unresolved-import",
format_args!(
"Cannot resolve import `{}{}`",
".".repeat(level as usize),
@@ -231,9 +630,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
}
pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) {
self.add(
self.add_lint(
&ZERO_STEPSIZE_IN_SLICE,
node,
"zero-stepsize-in-slice",
format_args!("Slice step size can not be zero"),
);
}
@@ -246,19 +645,19 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
) {
match declared_ty {
Type::ClassLiteral(ClassLiteralType { class }) => {
self.add(node, "invalid-assignment", format_args!(
self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
class.name(self.db)));
}
Type::FunctionLiteral(function) => {
self.add(node, "invalid-assignment", format_args!(
self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
function.name(self.db)));
}
_ => {
self.add(
self.add_lint(
&INVALID_ASSIGNMENT,
node,
"invalid-assignment",
format_args!(
"Object of type `{}` is not assignable to `{}`",
assigned_ty.display(self.db),
@@ -272,9 +671,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node;
self.add(
self.add_lint(
&POSSIBLY_UNRESOLVED_REFERENCE,
expr_name_node.into(),
"possibly-unresolved-reference",
format_args!("Name `{id}` used when possibly not defined"),
);
}
@@ -282,17 +681,37 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node;
self.add(
self.add_lint(
&UNRESOLVED_REFERENCE,
expr_name_node.into(),
"unresolved-reference",
format_args!("Name `{id}` used when not defined"),
);
}
pub(super) fn add_lint(
&mut self,
lint: &'static LintMetadata,
node: AnyNodeRef,
message: std::fmt::Arguments,
) {
// Skip over diagnostics if the rule is disabled.
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
return;
};
self.add(node, DiagnosticId::Lint(lint.name()), severity, message);
}
/// Adds a new diagnostic.
///
/// The diagnostic does not get added if the rule isn't enabled for this file.
pub(super) fn add(&mut self, node: AnyNodeRef, rule: &str, message: std::fmt::Arguments) {
pub(super) fn add(
&mut self,
node: AnyNodeRef,
id: DiagnosticId,
severity: Severity,
message: std::fmt::Arguments,
) {
if !self.db.is_file_open(self.file) {
return;
}
@@ -305,9 +724,10 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
self.diagnostics.push(TypeCheckDiagnostic {
file: self.file,
rule: rule.to_string(),
id,
message: message.to_string(),
range: node.range(),
severity,
});
}

View File

@@ -357,31 +357,10 @@ impl Display for DisplayStringLiteralType<'_> {
#[cfg(test)]
mod tests {
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::system::DbWithTestSystem;
use crate::db::tests::TestDb;
use crate::db::tests::setup_db;
use crate::types::{global_symbol, SliceLiteralType, StringLiteralType, Type, UnionType};
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
fn setup_db() -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(src_root),
},
)
.expect("Valid search path settings");
db
}
#[test]
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {

File diff suppressed because it is too large Load Diff

View File

@@ -328,15 +328,6 @@ impl<'db> ClassBase<'db> {
Display { base: self, db }
}
#[cfg(test)]
#[track_caller]
pub(super) fn expect_class_base(self) -> Class<'db> {
match self {
ClassBase::Class(class) => class,
_ => panic!("Expected a `ClassBase::Class()` variant"),
}
}
/// Return a `ClassBase` representing the class `builtins.object`
fn object(db: &'db dyn Db) -> Self {
KnownClass::Object
@@ -379,6 +370,7 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::NoReturn
| KnownInstanceType::Never
| KnownInstanceType::Optional => None,
KnownInstanceType::Any => Some(Self::Any),
},
}
}
@@ -390,14 +382,6 @@ impl<'db> ClassBase<'db> {
}
}
/// Returns `false` if the base is a non-fully-static type like `Any` or `Unknown`.
pub(crate) fn is_fully_static(self) -> bool {
match self {
Self::Class(_) => true,
Self::Any | Self::Unknown | Self::Todo => false,
}
}
/// Iterate over the MRO of this base
fn mro(
self,
@@ -414,6 +398,12 @@ impl<'db> ClassBase<'db> {
}
}
impl<'db> From<Class<'db>> for ClassBase<'db> {
fn from(value: Class<'db>) -> Self {
ClassBase::Class(value)
}
}
impl<'db> From<ClassBase<'db>> for Type<'db> {
fn from(value: ClassBase<'db>) -> Self {
match value {

View File

@@ -26,8 +26,8 @@
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use super::tests::{setup_db, Ty};
use crate::db::tests::TestDb;
use super::tests::Ty;
use crate::db::tests::{setup_db, TestDb};
use crate::types::KnownClass;
use quickcheck::{Arbitrary, Gen};

View File

@@ -189,32 +189,9 @@ impl<'db> Parameter<'db> {
#[cfg(test)]
mod tests {
use super::*;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, FunctionType};
use crate::ProgramSettings;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
pub(crate) fn setup_db() -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(src_root),
},
)
.expect("Valid search path settings");
db
}
use ruff_db::system::DbWithTestSystem;
#[track_caller]
fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> {

View File

@@ -5,8 +5,127 @@ use ruff_python_ast::{self as ast, ModExpression, StringFlags};
use ruff_python_parser::{parse_expression_range, Parsed};
use ruff_text_size::Ranged;
use crate::lint::{Level, LintStatus};
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
use crate::Db;
use crate::{declare_lint, Db};
declare_lint! {
/// ## What it does
/// Checks for f-strings in type annotation positions.
///
/// ## Why is this bad?
/// Static analysis tools like Red Knot can't analyse type annotations that use f-string notation.
///
/// ## Examples
/// ```python
/// def test(): -> f"int":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test(): -> "int":
/// ...
/// ```
pub(crate) static FSTRING_TYPE_ANNOTATION = {
summary: "detects F-strings in type annotation positions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for byte-strings in type annotation positions.
///
/// ## Why is this bad?
/// Static analysis tools like Red Knot can't analyse type annotations that use byte-string notation.
///
/// ## Examples
/// ```python
/// def test(): -> b"int":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test(): -> "int":
/// ...
/// ```
pub(crate) static BYTE_STRING_TYPE_ANNOTATION = {
summary: "detects byte strings in type annotation positions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for raw-strings in type annotation positions.
///
/// ## Why is this bad?
/// Static analysis tools like Red Knot can't analyse type annotations that use raw-string notation.
///
/// ## Examples
/// ```python
/// def test(): -> r"int":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test(): -> "int":
/// ...
/// ```
pub(crate) static RAW_STRING_TYPE_ANNOTATION = {
summary: "detects raw strings in type annotation positions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for implicit concatenated strings in type annotation positions.
///
/// ## Why is this bad?
/// Static analysis tools like Red Knot can't analyse type annotations that use implicit concatenated strings.
///
/// ## Examples
/// ```python
/// def test(): -> "Literal[" "5" "]":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test(): -> "Literal[5]":
/// ...
/// ```
pub(crate) static IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION = {
summary: "detects implicit concatenated strings in type annotations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_SYNTAX_IN_FORWARD_ANNOTATION = {
summary: "detects invalid syntax in forward annotations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION = {
summary: "detects forward type annotations with escape characters",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
type AnnotationParseResult = Result<Parsed<ModExpression>, TypeCheckDiagnostics>;
@@ -25,9 +144,9 @@ pub(crate) fn parse_string_annotation(
if let [string_literal] = string_expr.value.as_slice() {
let prefix = string_literal.flags.prefix();
if prefix.is_raw() {
diagnostics.add(
diagnostics.add_lint(
&RAW_STRING_TYPE_ANNOTATION,
string_literal.into(),
"annotation-raw-string",
format_args!("Type expressions cannot use raw string literal"),
);
// Compare the raw contents (without quotes) of the expression with the parsed contents
@@ -49,26 +168,26 @@ pub(crate) fn parse_string_annotation(
// ```
match parse_expression_range(source.as_str(), range_excluding_quotes) {
Ok(parsed) => return Ok(parsed),
Err(parse_error) => diagnostics.add(
Err(parse_error) => diagnostics.add_lint(
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
string_literal.into(),
"forward-annotation-syntax-error",
format_args!("Syntax error in forward annotation: {}", parse_error.error),
),
}
} else {
// The raw contents of the string doesn't match the parsed content. This could be the
// case for annotations that contain escape sequences.
diagnostics.add(
diagnostics.add_lint(
&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION,
string_expr.into(),
"annotation-escape-character",
format_args!("Type expressions cannot contain escape characters"),
);
}
} else {
// String is implicitly concatenated.
diagnostics.add(
diagnostics.add_lint(
&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
string_expr.into(),
"annotation-implicit-concat",
format_args!("Type expressions cannot span multiple string literals"),
);
}

View File

@@ -95,7 +95,8 @@ impl<'db> Unpacker<'db> {
// there would be a cost and it's not clear that it's worth it.
let value_ty = Type::tuple(
self.db,
std::iter::repeat(Type::LiteralString).take(string_literal_ty.len(self.db)),
std::iter::repeat(Type::LiteralString)
.take(string_literal_ty.python_len(self.db)),
);
self.unpack(target, value_ty, scope);
}

View File

@@ -48,7 +48,7 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
}
}
// TODO(dhruvmanila): Publish diagnostics if the client doesnt support pull diagnostics
// TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics
Ok(())
}

View File

@@ -86,14 +86,15 @@ fn to_lsp_diagnostic(
let severity = match diagnostic.severity() {
Severity::Info => DiagnosticSeverity::INFORMATION,
Severity::Error => DiagnosticSeverity::ERROR,
Severity::Warning => DiagnosticSeverity::WARNING,
Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR,
};
Diagnostic {
range,
severity: Some(severity),
tags: None,
code: Some(NumberOrString::String(diagnostic.rule().to_string())),
code: Some(NumberOrString::String(diagnostic.id().to_string())),
code_description: None,
source: Some("red-knot".into()),
message: diagnostic.message().into_owned(),

View File

@@ -27,6 +27,8 @@ regex = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
smallvec = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }
[dev-dependencies]

View File

@@ -225,6 +225,22 @@ A header-demarcated section must either be a test or a grouping header; it canno
a header section can either contain embedded files (making it a test), or it can contain more
deeply-nested headers (headers with more `#`), but it cannot contain both.
## Configuration
The test framework supports a TOML-based configuration format, which is a subset of the full red-knot
configuration format. This configuration can be specified in fenced code blocks with `toml` as the
language tag:
````markdown
```toml
[environment]
target-version = "3.10"
```
````
This configuration will apply to all tests in the same section, and all nested sections within that
section. Nested sections can override configurations from their parent sections.
## Documentation of tests
Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by
@@ -282,30 +298,6 @@ possible in these files.
A fenced code block with no language will always be an error.
### Configuration
We will add the ability to specify non-default red-knot configurations to use in tests, by including
a TOML code block:
````markdown
```toml
[tool.knot]
warn-on-any = true
```
```py
from typing import Any
def f(x: Any): # error: [use-of-any]
pass
```
````
It should be possible to include a TOML code block in a single test (as shown), or in a grouping
section, in which case it applies to all nested tests within that grouping section. Configurations
at multiple level are allowed and merged, with the most-nested (closest to the test) taking
precedence.
### Running just a single test from a suite
Having each test in a suite always run as a distinct Rust test would require writing our own test
@@ -317,11 +309,11 @@ variable.
### Configuring search paths and kinds
The red-knot TOML configuration format hasn't been designed yet, and we may want to implement
The red-knot TOML configuration format hasn't been finalized, and we may want to implement
support in the test framework for configuring search paths before it is designed. If so, we can
define some configuration options for now under the `[tool.knot.tests]` namespace. In the future,
perhaps some of these can be replaced by real red-knot configuration options; some or all may also
be kept long-term as test-specific options.
define some configuration options for now under the `[tests]` namespace. In the future, perhaps
some of these can be replaced by real red-knot configuration options; some or all may also be
kept long-term as test-specific options.
Some configuration options we will want to provide:
@@ -339,13 +331,13 @@ non-default value using the `workspace-root` config.
### Specifying a custom typeshed
Some tests will need to override the default typeshed with custom files. The `[tool.knot.tests]`
configuration option `typeshed-root` should be usable for this:
Some tests will need to override the default typeshed with custom files. The `[environment]`
configuration option `typeshed-path` can be used to do this:
````markdown
```toml
[tool.knot.tests]
typeshed-root = "/typeshed"
[environment]
typeshed-path = "/typeshed"
```
This file is importable as part of our custom typeshed, because it is within `/typeshed`, which we

View File

@@ -0,0 +1,28 @@
//! TOML-deserializable Red Knot configuration, similar to `knot.toml`, to be able to
//! control some configuration options from Markdown files. For now, this supports the
//! following limited structure:
//!
//! ```toml
//! [environment]
//! target-version = "3.10"
//! ```
use anyhow::Context;
use serde::Deserialize;
#[derive(Deserialize)]
pub(crate) struct MarkdownTestConfig {
pub(crate) environment: Environment,
}
impl MarkdownTestConfig {
pub(crate) fn from_str(s: &str) -> anyhow::Result<Self> {
toml::from_str(s).context("Error while parsing Markdown TOML config")
}
}
#[derive(Deserialize)]
pub(crate) struct Environment {
#[serde(rename = "target-version")]
pub(crate) target_version: String,
}

View File

@@ -1,5 +1,7 @@
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{
Db as SemanticDb, Program, ProgramSettings, PythonVersion, SearchPathSettings,
default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonVersion,
SearchPathSettings,
};
use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem};
@@ -13,16 +15,20 @@ pub(crate) struct Db {
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
rule_selection: RuleSelection,
}
impl Db {
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
let rule_selection = RuleSelection::from_registry(&default_lint_registry());
let db = Self {
workspace_root,
storage: salsa::Storage::default(),
system: TestSystem::default(),
vendored: red_knot_vendored::file_system().clone(),
files: Files::default(),
rule_selection,
};
db.memory_file_system()
@@ -85,6 +91,10 @@ impl SemanticDb for Db {
fn is_file_open(&self, file: File) -> bool {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
}
#[salsa::db]

View File

@@ -144,7 +144,7 @@ struct DiagnosticWithLine<T> {
mod tests {
use crate::db::Db;
use crate::diagnostic::Diagnostic;
use ruff_db::diagnostic::Severity;
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::source::line_index;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@@ -190,8 +190,8 @@ mod tests {
}
impl Diagnostic for DummyDiagnostic {
fn rule(&self) -> &str {
"dummy"
fn id(&self) -> DiagnosticId {
DiagnosticId::Lint(LintName::of("dummy"))
}
fn message(&self) -> Cow<str> {

View File

@@ -2,14 +2,17 @@ use camino::Utf8Path;
use colored::Colorize;
use parser as test_parser;
use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::Program;
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
use ruff_db::files::{system_path_to_file, File, Files};
use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_source_file::LineIndex;
use ruff_text_size::TextSize;
use salsa::Setter;
mod assertion;
mod config;
mod db;
mod diagnostic;
mod matcher;
@@ -26,7 +29,7 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
let suite = match test_parser::parse(short_title, &source) {
Ok(suite) => suite,
Err(err) => {
panic!("Error parsing `{path}`: {err}")
panic!("Error parsing `{path}`: {err:?}")
}
};
@@ -39,6 +42,10 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
continue;
}
Program::get(&db)
.set_target_version(&mut db)
.to(test.target_version());
// Remove all files so that the db is in a "fresh" state.
db.memory_file_system().remove_all();
Files::sync_all(&mut db);

View File

@@ -4,7 +4,7 @@ use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
use crate::db::Db;
use crate::diagnostic::SortedDiagnostics;
use colored::Colorize;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
use ruff_db::files::File;
use ruff_db::source::{line_index, source_text, SourceText};
use ruff_source_file::{LineIndex, OneIndexed};
@@ -146,7 +146,7 @@ fn maybe_add_undefined_reveal_clarification<T: Diagnostic>(
diagnostic: &T,
original: std::fmt::Arguments,
) -> String {
if diagnostic.rule() == "undefined-reveal" {
if diagnostic.id().is_lint_named("undefined-reveal") {
format!(
"{} add a `# revealed` assertion on this line (original diagnostic: {original})",
"used built-in `reveal_type`:".yellow()
@@ -163,7 +163,7 @@ where
fn unmatched(&self) -> String {
maybe_add_undefined_reveal_clarification(
self,
format_args!(r#"[{}] "{}""#, self.rule(), self.message()),
format_args!(r#"[{}] "{}""#, self.id(), self.message()),
)
}
}
@@ -175,7 +175,7 @@ where
fn unmatched_with_column(&self, column: OneIndexed) -> String {
maybe_add_undefined_reveal_clarification(
self,
format_args!(r#"{column} [{}] "{}""#, self.rule(), self.message()),
format_args!(r#"{column} [{}] "{}""#, self.id(), self.message()),
)
}
}
@@ -270,10 +270,11 @@ impl Matcher {
match assertion {
Assertion::Error(error) => {
let position = unmatched.iter().position(|diagnostic| {
!error.rule.is_some_and(|rule| rule != diagnostic.rule())
&& !error
.column
.is_some_and(|col| col != self.column(*diagnostic))
!error.rule.is_some_and(|rule| {
!(diagnostic.id().is_lint_named(rule) || diagnostic.id().matches(rule))
}) && !error
.column
.is_some_and(|col| col != self.column(*diagnostic))
&& !error
.message_contains
.is_some_and(|needle| !diagnostic.message().contains(needle))
@@ -294,12 +295,12 @@ impl Matcher {
let expected_reveal_type_message = format!("Revealed type is `{expected_type}`");
for (index, diagnostic) in unmatched.iter().enumerate() {
if matched_revealed_type.is_none()
&& diagnostic.rule() == "revealed-type"
&& diagnostic.id() == DiagnosticId::RevealedType
&& diagnostic.message() == expected_reveal_type_message
{
matched_revealed_type = Some(index);
} else if matched_undefined_reveal.is_none()
&& diagnostic.rule() == "undefined-reveal"
&& diagnostic.id().is_lint_named("undefined-reveal")
{
matched_undefined_reveal = Some(index);
}
@@ -323,7 +324,7 @@ impl Matcher {
#[cfg(test)]
mod tests {
use super::FailuresByLine;
use ruff_db::diagnostic::{Diagnostic, Severity};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_python_trivia::textwrap::dedent;
@@ -332,16 +333,16 @@ mod tests {
use std::borrow::Cow;
struct ExpectedDiagnostic {
rule: &'static str,
id: DiagnosticId,
message: &'static str,
range: TextRange,
}
impl ExpectedDiagnostic {
fn new(rule: &'static str, message: &'static str, offset: usize) -> Self {
fn new(id: DiagnosticId, message: &'static str, offset: usize) -> Self {
let offset: u32 = offset.try_into().unwrap();
Self {
rule,
id,
message,
range: TextRange::new(offset.into(), (offset + 1).into()),
}
@@ -349,7 +350,7 @@ mod tests {
fn into_diagnostic(self, file: File) -> TestDiagnostic {
TestDiagnostic {
rule: self.rule,
id: self.id,
message: self.message,
range: self.range,
file,
@@ -359,15 +360,15 @@ mod tests {
#[derive(Debug)]
struct TestDiagnostic {
rule: &'static str,
id: DiagnosticId,
message: &'static str,
range: TextRange,
file: File,
}
impl Diagnostic for TestDiagnostic {
fn rule(&self) -> &str {
self.rule
fn id(&self) -> DiagnosticId {
self.id
}
fn message(&self) -> Cow<str> {
@@ -437,7 +438,7 @@ mod tests {
let result = get_result(
"x # revealed: Foo",
vec![ExpectedDiagnostic::new(
"revealed-type",
DiagnosticId::RevealedType,
"Revealed type is `Foo`",
0,
)],
@@ -451,7 +452,7 @@ mod tests {
let result = get_result(
"x # revealed: Foo",
vec![ExpectedDiagnostic::new(
"not-revealed-type",
DiagnosticId::lint("not-revealed-type"),
"Revealed type is `Foo`",
0,
)],
@@ -463,7 +464,7 @@ mod tests {
0,
&[
"unmatched assertion: revealed: Foo",
r#"unexpected error: 1 [not-revealed-type] "Revealed type is `Foo`""#,
r#"unexpected error: 1 [lint/not-revealed-type] "Revealed type is `Foo`""#,
],
)],
);
@@ -474,7 +475,7 @@ mod tests {
let result = get_result(
"x # revealed: Foo",
vec![ExpectedDiagnostic::new(
"revealed-type",
DiagnosticId::RevealedType,
"Something else",
0,
)],
@@ -504,8 +505,12 @@ mod tests {
let result = get_result(
"x # revealed: Foo",
vec![
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "Revealed type is `Foo`", 0),
ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"Doesn't matter",
0,
),
],
);
@@ -517,7 +522,7 @@ mod tests {
let result = get_result(
"x # revealed: Foo",
vec![ExpectedDiagnostic::new(
"undefined-reveal",
DiagnosticId::lint("undefined-reveal"),
"Doesn't matter",
0,
)],
@@ -531,8 +536,12 @@ mod tests {
let result = get_result(
"x # revealed: Foo",
vec![
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "Revealed type is `Bar`", 0),
ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"Doesn't matter",
0,
),
],
);
@@ -553,8 +562,16 @@ mod tests {
let result = get_result(
"reveal_type(1)",
vec![
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"undefined reveal message",
0,
),
ExpectedDiagnostic::new(
DiagnosticId::RevealedType,
"Revealed type is `Literal[1]`",
12,
),
],
);
@@ -564,7 +581,7 @@ mod tests {
0,
&[
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
original diagnostic: [undefined-reveal] \"undefined reveal message\")",
original diagnostic: [lint/undefined-reveal] \"undefined reveal message\")",
r#"unexpected error: [revealed-type] "Revealed type is `Literal[1]`""#,
],
)],
@@ -576,8 +593,16 @@ mod tests {
let result = get_result(
"reveal_type(1) # error: [something-else]",
vec![
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"undefined reveal message",
0,
),
ExpectedDiagnostic::new(
DiagnosticId::RevealedType,
"Revealed type is `Literal[1]`",
12,
),
],
);
@@ -588,7 +613,7 @@ mod tests {
&[
"unmatched assertion: error: [something-else]",
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
original diagnostic: 1 [undefined-reveal] \"undefined reveal message\")",
original diagnostic: 1 [lint/undefined-reveal] \"undefined reveal message\")",
r#"unexpected error: 13 [revealed-type] "Revealed type is `Literal[1]`""#,
],
)],
@@ -606,7 +631,11 @@ mod tests {
fn error_match_rule() {
let result = get_result(
"x # error: [some-rule]",
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
);
assert_ok(&result);
@@ -616,7 +645,11 @@ mod tests {
fn error_wrong_rule() {
let result = get_result(
"x # error: [some-rule]",
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("anything"),
"Any message",
0,
)],
);
assert_fail(
@@ -625,7 +658,7 @@ mod tests {
0,
&[
"unmatched assertion: error: [some-rule]",
r#"unexpected error: 1 [anything] "Any message""#,
r#"unexpected error: 1 [lint/anything] "Any message""#,
],
)],
);
@@ -636,7 +669,7 @@ mod tests {
let result = get_result(
r#"x # error: "contains this""#,
vec![ExpectedDiagnostic::new(
"anything",
DiagnosticId::lint("anything"),
"message contains this",
0,
)],
@@ -649,7 +682,11 @@ mod tests {
fn error_wrong_message() {
let result = get_result(
r#"x # error: "contains this""#,
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("anything"),
"Any message",
0,
)],
);
assert_fail(
@@ -658,7 +695,7 @@ mod tests {
0,
&[
r#"unmatched assertion: error: "contains this""#,
r#"unexpected error: 1 [anything] "Any message""#,
r#"unexpected error: 1 [lint/anything] "Any message""#,
],
)],
);
@@ -668,7 +705,11 @@ mod tests {
fn error_match_column_and_rule() {
let result = get_result(
"x # error: 1 [some-rule]",
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
);
assert_ok(&result);
@@ -678,7 +719,11 @@ mod tests {
fn error_wrong_column() {
let result = get_result(
"x # error: 2 [rule]",
vec![ExpectedDiagnostic::new("rule", "Any message", 0)],
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("rule"),
"Any message",
0,
)],
);
assert_fail(
@@ -687,7 +732,7 @@ mod tests {
0,
&[
"unmatched assertion: error: 2 [rule]",
r#"unexpected error: 1 [rule] "Any message""#,
r#"unexpected error: 1 [lint/rule] "Any message""#,
],
)],
);
@@ -698,7 +743,7 @@ mod tests {
let result = get_result(
r#"x # error: 1 "contains this""#,
vec![ExpectedDiagnostic::new(
"anything",
DiagnosticId::lint("anything"),
"message contains this",
0,
)],
@@ -712,7 +757,7 @@ mod tests {
let result = get_result(
r#"x # error: [a-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
"a-rule",
DiagnosticId::lint("a-rule"),
"message contains this",
0,
)],
@@ -726,7 +771,7 @@ mod tests {
let result = get_result(
r#"x # error: 1 [a-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
"a-rule",
DiagnosticId::lint("a-rule"),
"message contains this",
0,
)],
@@ -740,7 +785,7 @@ mod tests {
let result = get_result(
r#"x # error: 2 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
"some-rule",
DiagnosticId::lint("some-rule"),
"message contains this",
0,
)],
@@ -752,7 +797,7 @@ mod tests {
0,
&[
r#"unmatched assertion: error: 2 [some-rule] "contains this""#,
r#"unexpected error: 1 [some-rule] "message contains this""#,
r#"unexpected error: 1 [lint/some-rule] "message contains this""#,
],
)],
);
@@ -763,7 +808,7 @@ mod tests {
let result = get_result(
r#"x # error: 1 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
"other-rule",
DiagnosticId::lint("other-rule"),
"message contains this",
0,
)],
@@ -775,7 +820,7 @@ mod tests {
0,
&[
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
r#"unexpected error: 1 [other-rule] "message contains this""#,
r#"unexpected error: 1 [lint/other-rule] "message contains this""#,
],
)],
);
@@ -785,7 +830,11 @@ mod tests {
fn error_match_all_wrong_message() {
let result = get_result(
r#"x # error: 1 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
);
assert_fail(
@@ -794,7 +843,7 @@ mod tests {
0,
&[
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
r#"unexpected error: 1 [some-rule] "Any message""#,
r#"unexpected error: 1 [lint/some-rule] "Any message""#,
],
)],
);
@@ -818,9 +867,9 @@ mod tests {
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new("line-two", "msg", two),
ExpectedDiagnostic::new("line-three", "msg", three),
ExpectedDiagnostic::new("line-five", "msg", five),
ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
ExpectedDiagnostic::new(DiagnosticId::lint("line-three"), "msg", three),
ExpectedDiagnostic::new(DiagnosticId::lint("line-five"), "msg", five),
],
);
@@ -828,9 +877,9 @@ mod tests {
result,
&[
(1, &["unmatched assertion: error: [line-one]"]),
(2, &[r#"unexpected error: [line-two] "msg""#]),
(2, &[r#"unexpected error: [lint/line-two] "msg""#]),
(4, &["unmatched assertion: error: [line-four]"]),
(5, &[r#"unexpected error: [line-five] "msg""#]),
(5, &[r#"unexpected error: [lint/line-five] "msg""#]),
(6, &["unmatched assertion: error: [line-six]"]),
],
);
@@ -849,12 +898,15 @@ mod tests {
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new("line-one", "msg", one),
ExpectedDiagnostic::new("line-two", "msg", two),
ExpectedDiagnostic::new(DiagnosticId::lint("line-one"), "msg", one),
ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
],
);
assert_fail(result, &[(2, &[r#"unexpected error: [line-two] "msg""#])]);
assert_fail(
result,
&[(2, &[r#"unexpected error: [lint/line-two] "msg""#])],
);
}
#[test]
@@ -870,8 +922,8 @@ mod tests {
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new("one-rule", "msg", x),
ExpectedDiagnostic::new("other-rule", "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
],
);
@@ -891,8 +943,8 @@ mod tests {
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new("one-rule", "msg", x),
ExpectedDiagnostic::new("one-rule", "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
],
);
@@ -912,15 +964,15 @@ mod tests {
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new("one-rule", "msg", x),
ExpectedDiagnostic::new("other-rule", "msg", x),
ExpectedDiagnostic::new("third-rule", "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("third-rule"), "msg", x),
],
);
assert_fail(
result,
&[(3, &[r#"unexpected error: 1 [third-rule] "msg""#])],
&[(3, &[r#"unexpected error: 1 [lint/third-rule] "msg""#])],
);
}
@@ -938,8 +990,12 @@ mod tests {
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new("undefined-reveal", "msg", reveal),
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
ExpectedDiagnostic::new(DiagnosticId::lint("undefined-reveal"), "msg", reveal),
ExpectedDiagnostic::new(
DiagnosticId::RevealedType,
"Revealed type is `Literal[5]`",
reveal,
),
],
);
@@ -952,7 +1008,11 @@ mod tests {
let x = source.find('x').unwrap();
let result = get_result(
source,
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
x,
)],
);
assert_fail(
@@ -961,7 +1021,7 @@ mod tests {
0,
&[
"invalid assertion: no rule or message text",
r#"unexpected error: 1 [some-rule] "some message""#,
r#"unexpected error: 1 [lint/some-rule] "some message""#,
],
)],
);
@@ -973,7 +1033,11 @@ mod tests {
let x = source.find('x').unwrap();
let result = get_result(
source,
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
x,
)],
);
assert_fail(
@@ -982,7 +1046,7 @@ mod tests {
0,
&[
"invalid assertion: no rule or message text",
r#"unexpected error: 1 [some-rule] "some message""#,
r#"unexpected error: 1 [lint/some-rule] "some message""#,
],
)],
);

View File

@@ -1,6 +1,8 @@
use std::sync::LazyLock;
use anyhow::{bail, Context};
use memchr::memchr2;
use red_knot_python_semantic::PythonVersion;
use regex::{Captures, Match, Regex};
use rustc_hash::{FxHashMap, FxHashSet};
@@ -8,6 +10,8 @@ use ruff_index::{newtype_index, IndexVec};
use ruff_python_trivia::Cursor;
use ruff_text_size::{TextLen, TextSize};
use crate::config::MarkdownTestConfig;
/// Parse the Markdown `source` as a test suite with given `title`.
pub(crate) fn parse<'s>(title: &'s str, source: &'s str) -> anyhow::Result<MarkdownTestSuite<'s>> {
let parser = Parser::new(title, source);
@@ -69,6 +73,10 @@ impl<'m, 's> MarkdownTest<'m, 's> {
pub(crate) fn files(&self) -> impl Iterator<Item = &'m EmbeddedFile<'s>> {
self.files.iter()
}
pub(crate) fn target_version(&self) -> PythonVersion {
self.section.target_version
}
}
/// Iterator yielding all [`MarkdownTest`]s in a [`MarkdownTestSuite`].
@@ -117,6 +125,7 @@ struct Section<'s> {
title: &'s str,
level: u8,
parent_id: Option<SectionId>,
target_version: PythonVersion,
}
#[newtype_index]
@@ -174,7 +183,7 @@ impl SectionStack {
popped
}
fn parent(&mut self) -> SectionId {
fn top(&mut self) -> SectionId {
*self
.0
.last()
@@ -201,6 +210,9 @@ struct Parser<'s> {
/// Names of embedded files in current active section.
current_section_files: Option<FxHashSet<&'s str>>,
/// Whether or not the current section has a config block.
current_section_has_config: bool,
}
impl<'s> Parser<'s> {
@@ -210,6 +222,7 @@ impl<'s> Parser<'s> {
title,
level: 0,
parent_id: None,
target_version: PythonVersion::default(),
});
Self {
sections,
@@ -218,6 +231,7 @@ impl<'s> Parser<'s> {
source_len: source.text_len(),
stack: SectionStack::new(root_section_id),
current_section_files: None,
current_section_has_config: false,
}
}
@@ -285,12 +299,13 @@ impl<'s> Parser<'s> {
self.pop_sections_to_level(header_level);
let parent = self.stack.parent();
let parent = self.stack.top();
let section = Section {
title,
level: header_level.try_into()?,
parent_id: Some(parent),
target_version: self.sections[parent].target_version,
};
if self.current_section_files.is_some() {
@@ -305,13 +320,14 @@ impl<'s> Parser<'s> {
self.stack.push(section_id);
self.current_section_files = None;
self.current_section_has_config = false;
Ok(())
}
fn parse_code_block(&mut self, captures: &Captures<'s>) -> anyhow::Result<()> {
// We never pop the implicit root section.
let parent = self.stack.parent();
let section = self.stack.top();
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default();
@@ -333,16 +349,24 @@ impl<'s> Parser<'s> {
let path = config.get("path").copied().unwrap_or("test.py");
// CODE_RE can't match without matches for 'lang' and 'code'.
let lang = captures
.name("lang")
.as_ref()
.map(Match::as_str)
.unwrap_or_default();
let code = captures.name("code").unwrap().into();
if lang == "toml" {
return self.parse_config(code);
}
self.files.push(EmbeddedFile {
path,
section: parent,
lang: captures
.name("lang")
.as_ref()
.map(Match::as_str)
.unwrap_or_default(),
// CODE_RE can't match without matches for 'lang' and 'code'.
code: captures.name("code").unwrap().into(),
section,
lang,
code,
md_offset: self.offset(),
});
@@ -354,12 +378,12 @@ impl<'s> Parser<'s> {
"Test `{}` has duplicate files named `{path}`. \
(This is the default filename; \
consider giving some files an explicit name with `path=...`.)",
self.sections[parent].title
self.sections[section].title
));
}
return Err(anyhow::anyhow!(
"Test `{}` has duplicate files named `{path}`.",
self.sections[parent].title
self.sections[section].title
));
};
} else {
@@ -369,8 +393,36 @@ impl<'s> Parser<'s> {
Ok(())
}
fn parse_config(&mut self, code: &str) -> anyhow::Result<()> {
if self.current_section_has_config {
bail!("Multiple TOML configuration blocks in the same section are not allowed.");
}
let config = MarkdownTestConfig::from_str(code)?;
let target_version = config.environment.target_version;
let parts = target_version
.split('.')
.map(str::parse)
.collect::<Result<Vec<_>, _>>()
.context(format!(
"Invalid 'target-version' component: '{target_version}'"
))?;
if parts.len() != 2 {
bail!("Invalid 'target-version': expected MAJOR.MINOR, got '{target_version}'.",);
}
let current_section = &mut self.sections[self.stack.top()];
current_section.target_version = PythonVersion::from((parts[0], parts[1]));
self.current_section_has_config = true;
Ok(())
}
fn pop_sections_to_level(&mut self, level: usize) {
while level <= self.sections[self.stack.parent()].level.into() {
while level <= self.sections[self.stack.top()].level.into() {
self.stack.pop();
// We would have errored before pushing a child section if there were files, so we know
// no parent section can have files.

View File

@@ -1,16 +1,17 @@
use std::panic::RefUnwindSafe;
use std::sync::Arc;
use salsa::plumbing::ZalsaDatabase;
use salsa::{Cancelled, Event};
use crate::workspace::{check_file, Workspace, WorkspaceMetadata};
use crate::DEFAULT_LINT_REGISTRY;
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{Db as SemanticDb, Program};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files};
use ruff_db::system::System;
use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast};
use salsa::plumbing::ZalsaDatabase;
use salsa::{Cancelled, Event};
mod changes;
@@ -25,6 +26,7 @@ pub struct RootDatabase {
storage: salsa::Storage<RootDatabase>,
files: Files,
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
rule_selection: Arc<RuleSelection>,
}
impl RootDatabase {
@@ -32,11 +34,14 @@ impl RootDatabase {
where
S: System + 'static + Send + Sync + RefUnwindSafe,
{
let rule_selection = RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY);
let mut db = Self {
workspace: None,
storage: salsa::Storage::default(),
files: Files::default(),
system: Arc::new(system),
rule_selection: Arc::new(rule_selection),
};
// Initialize the `Program` singleton
@@ -83,6 +88,7 @@ impl RootDatabase {
storage: self.storage.clone(),
files: self.files.snapshot(),
system: Arc::clone(&self.system),
rule_selection: Arc::clone(&self.rule_selection),
}
}
}
@@ -116,6 +122,10 @@ impl SemanticDb for RootDatabase {
workspace.is_file_open(self, file)
}
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
}
#[salsa::db]
@@ -162,6 +172,7 @@ pub(crate) mod tests {
use salsa::Event;
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::Db as SemanticDb;
use ruff_db::files::Files;
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
@@ -170,14 +181,16 @@ pub(crate) mod tests {
use crate::db::Db;
use crate::workspace::{Workspace, WorkspaceMetadata};
use crate::DEFAULT_LINT_REGISTRY;
#[salsa::db]
pub(crate) struct TestDb {
storage: salsa::Storage<Self>,
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
events: Arc<std::sync::Mutex<Vec<Event>>>,
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
rule_selection: RuleSelection,
workspace: Option<Workspace>,
}
@@ -189,6 +202,7 @@ pub(crate) mod tests {
vendored: red_knot_vendored::file_system().clone(),
files: Files::default(),
events: Arc::default(),
rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),
workspace: None,
};
@@ -259,6 +273,10 @@ pub(crate) mod tests {
fn is_file_open(&self, file: ruff_db::files::File) -> bool {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
}
#[salsa::db]

View File

@@ -1,3 +1,15 @@
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder};
use red_knot_python_semantic::register_semantic_lints;
pub mod db;
pub mod watch;
pub mod workspace;
pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock<LintRegistry> =
std::sync::LazyLock::new(default_lints_registry);
pub fn default_lints_registry() -> LintRegistry {
let mut builder = LintRegistryBuilder::default();
register_semantic_lints(&mut builder);
builder.build()
}

View File

@@ -6,7 +6,7 @@ use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
pub use metadata::{PackageMetadata, WorkspaceDiscoveryError, WorkspaceMetadata};
use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::SearchPathSettings;
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic, Severity};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
use ruff_db::parsed::parsed_module;
use ruff_db::source::{source_text, SourceTextError};
use ruff_db::system::FileType;
@@ -533,8 +533,8 @@ pub struct IOErrorDiagnostic {
}
impl Diagnostic for IOErrorDiagnostic {
fn rule(&self) -> &str {
"io"
fn id(&self) -> DiagnosticId {
DiagnosticId::Io
}
fn message(&self) -> Cow<str> {

View File

@@ -694,7 +694,7 @@ mod tests {
/// Folders that match the members pattern but don't have a pyproject.toml
/// aren't valid members and discovery fails. However, don't fail
/// if the folder name indicates that it is a hidden folder that might
/// have been crated by another tool
/// have been created by another tool
#[test]
fn member_pattern_matching_hidden_folder() -> anyhow::Result<()> {
let system = TestSystem::default();

View File

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

View File

@@ -64,11 +64,19 @@ enum ChangeKind {
/// Return the [`ChangeKind`] based on the list of modified file paths.
///
/// Returns `None` if no relevant changes were detected.
fn change_detected(paths: &[PathBuf]) -> Option<ChangeKind> {
fn change_detected(event: &notify::Event) -> Option<ChangeKind> {
// If any `.toml` files were modified, return `ChangeKind::Configuration`. Otherwise, return
// `ChangeKind::SourceFile` if any `.py`, `.pyi`, `.pyw`, or `.ipynb` files were modified.
let mut source_file = false;
for path in paths {
if event.kind.is_access() || event.kind.is_other() {
return None;
}
if event.need_rescan() {
return Some(ChangeKind::Configuration);
}
for path in &event.paths {
if let Some(suffix) = path.extension() {
match suffix.to_str() {
Some("toml") => {
@@ -377,7 +385,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
loop {
match rx.recv() {
Ok(event) => {
let Some(change_kind) = change_detected(&event?.paths) else {
let Some(change_kind) = change_detected(&event?) else {
continue;
};
@@ -483,73 +491,113 @@ mod test_file_change_detector {
fn detect_correct_file_change() {
assert_eq!(
Some(ChangeKind::Configuration),
change_detected(&[
PathBuf::from("tmp/pyproject.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("tmp/pyproject.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
Some(ChangeKind::Configuration),
change_detected(&[
PathBuf::from("pyproject.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("pyproject.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
Some(ChangeKind::Configuration),
change_detected(&[
PathBuf::from("tmp1/tmp2/tmp3/pyproject.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("tmp1/tmp2/tmp3/pyproject.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
Some(ChangeKind::Configuration),
change_detected(&[
PathBuf::from("tmp/ruff.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("tmp/ruff.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
Some(ChangeKind::Configuration),
change_detected(&[
PathBuf::from("tmp/.ruff.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("tmp/.ruff.toml"),
PathBuf::from("tmp/bin/ruff.rs"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
Some(ChangeKind::SourceFile),
change_detected(&[
PathBuf::from("tmp/rule.py"),
PathBuf::from("tmp/bin/ruff.rs"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("tmp/rule.py"),
PathBuf::from("tmp/bin/ruff.rs"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
Some(ChangeKind::SourceFile),
change_detected(&[
PathBuf::from("tmp/rule.pyi"),
PathBuf::from("tmp/bin/ruff.rs"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("tmp/rule.pyi"),
PathBuf::from("tmp/bin/ruff.rs"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
Some(ChangeKind::Configuration),
change_detected(&[
PathBuf::from("pyproject.toml"),
PathBuf::from("tmp/rule.py"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("pyproject.toml"),
PathBuf::from("tmp/rule.py"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
Some(ChangeKind::Configuration),
change_detected(&[
PathBuf::from("tmp/rule.py"),
PathBuf::from("pyproject.toml"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("tmp/rule.py"),
PathBuf::from("pyproject.toml"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
assert_eq!(
None,
change_detected(&[
PathBuf::from("tmp/rule.js"),
PathBuf::from("tmp/bin/ruff.rs"),
]),
change_detected(&notify::Event {
kind: notify::EventKind::Create(notify::event::CreateKind::File),
paths: vec![
PathBuf::from("tmp/rule.js"),
PathBuf::from("tmp/bin/ruff.rs"),
],
attrs: notify::event::EventAttributes::default(),
}),
);
}
}

View File

@@ -1,14 +1,125 @@
use std::borrow::Cow;
use std::fmt::Formatter;
use ruff_python_parser::ParseError;
use ruff_text_size::TextRange;
use crate::{
files::File,
source::{line_index, source_text},
Db,
};
use ruff_python_parser::ParseError;
use ruff_text_size::TextRange;
use std::borrow::Cow;
/// A string identifier for a lint rule.
///
/// This string is used in command line and configuration interfaces. The name should always
/// be in kebab case, e.g. `no-foo` (all lower case).
///
/// Rules use kebab case, e.g. `no-foo`.
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct LintName(&'static str);
impl LintName {
pub const fn of(name: &'static str) -> Self {
Self(name)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
impl std::ops::Deref for LintName {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl std::fmt::Display for LintName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl PartialEq<str> for LintName {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for LintName {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
/// Uniquely identifies the kind of a diagnostic.
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub enum DiagnosticId {
/// Some I/O operation failed
Io,
/// Some code contains a syntax error
InvalidSyntax,
/// A lint violation.
///
/// Lint's can be suppressed and some lints can be enabled or disabled in the configuration.
Lint(LintName),
/// Some code is incorrectly formatted.
Format,
/// A revealed type: Created by `reveal_type(expression)`.
RevealedType,
}
impl DiagnosticId {
/// Creates a new `DiagnosticId` for a lint with the given name.
pub const fn lint(name: &'static str) -> Self {
Self::Lint(LintName::of(name))
}
/// Returns `true` if this `DiagnosticId` represents a lint.
pub fn is_lint(&self) -> bool {
matches!(self, DiagnosticId::Lint(_))
}
/// Returns `true` if this `DiagnosticId` represents a lint with the given name.
pub fn is_lint_named(&self, name: &str) -> bool {
matches!(self, DiagnosticId::Lint(self_name) if self_name == name)
}
pub fn matches(&self, name: &str) -> bool {
match self {
DiagnosticId::Lint(self_name) => name
.strip_prefix("lint/")
.is_some_and(|rest| rest == &**self_name),
DiagnosticId::Io => name == "io",
DiagnosticId::InvalidSyntax => name == "invalid-syntax",
DiagnosticId::Format => name == "format",
DiagnosticId::RevealedType => name == "revealed-type",
}
}
}
impl std::fmt::Display for DiagnosticId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
DiagnosticId::InvalidSyntax => f.write_str("invalid-syntax"),
DiagnosticId::Io => f.write_str("io"),
DiagnosticId::Lint(name) => write!(f, "lint/{name}"),
DiagnosticId::Format => f.write_str("format"),
DiagnosticId::RevealedType => f.write_str("revealed-type"),
}
}
}
pub trait Diagnostic: Send + Sync + std::fmt::Debug {
fn rule(&self) -> &str;
fn id(&self) -> DiagnosticId;
fn message(&self) -> std::borrow::Cow<str>;
@@ -29,10 +140,12 @@ pub trait Diagnostic: Send + Sync + std::fmt::Debug {
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
pub enum Severity {
Info,
Warning,
Error,
Fatal,
}
pub struct DisplayDiagnostic<'db> {
@@ -50,13 +163,15 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.diagnostic.severity() {
Severity::Info => f.write_str("info")?,
Severity::Warning => f.write_str("warning")?,
Severity::Error => f.write_str("error")?,
Severity::Fatal => f.write_str("fatal")?,
}
write!(
f,
"[{rule}] {path}",
rule = self.diagnostic.rule(),
rule = self.diagnostic.id(),
path = self.diagnostic.file().path(self.db)
)?;
@@ -77,8 +192,8 @@ impl<T> Diagnostic for Box<T>
where
T: Diagnostic,
{
fn rule(&self) -> &str {
(**self).rule()
fn id(&self) -> DiagnosticId {
(**self).id()
}
fn message(&self) -> Cow<str> {
@@ -102,8 +217,8 @@ impl<T> Diagnostic for std::sync::Arc<T>
where
T: Diagnostic,
{
fn rule(&self) -> &str {
(**self).rule()
fn id(&self) -> DiagnosticId {
(**self).id()
}
fn message(&self) -> std::borrow::Cow<str> {
@@ -124,8 +239,8 @@ where
}
impl Diagnostic for Box<dyn Diagnostic> {
fn rule(&self) -> &str {
(**self).rule()
fn id(&self) -> DiagnosticId {
(**self).id()
}
fn message(&self) -> Cow<str> {
@@ -158,8 +273,8 @@ impl ParseDiagnostic {
}
impl Diagnostic for ParseDiagnostic {
fn rule(&self) -> &str {
"invalid-syntax"
fn id(&self) -> DiagnosticId {
DiagnosticId::InvalidSyntax
}
fn message(&self) -> Cow<str> {

View File

@@ -299,7 +299,8 @@ where
///
/// - Removes [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::Soft).
/// - Replaces [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::SoftOrSpace) with a [`Space`](FormatElement::Space)
/// - Removes [`if_group_breaks`](crate::builders::if_group_breaks) elements.
/// - Removes [`if_group_breaks`](crate::builders::if_group_breaks) and all its content.
/// - Unwraps the content of [`if_group_fits_on_line`](crate::builders::if_group_fits_on_line) elements (but retains it).
///
/// # Examples
///
@@ -387,7 +388,7 @@ fn clean_interned(
.iter()
.enumerate()
.find_map(|(index, element)| match element {
FormatElement::Line(LineMode::Soft | LineMode::SoftOrSpace) => {
FormatElement::Line(LineMode::SoftOrSpace) => {
let mut cleaned = Vec::new();
let (before, after) = interned.split_at(index);
cleaned.extend_from_slice(before);
@@ -427,7 +428,6 @@ fn clean_interned(
}
let element = match element {
FormatElement::Line(LineMode::Soft) => continue,
FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space,
FormatElement::Interned(interned) => {
FormatElement::Interned(clean_interned(interned, interned_cache))
@@ -458,7 +458,6 @@ impl<Context> Buffer for RemoveSoftLinesBuffer<'_, Context> {
}
let element = match element {
FormatElement::Line(LineMode::Soft) => return,
FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space,
FormatElement::Interned(interned) => {
FormatElement::Interned(self.clean_interned(&interned))
@@ -508,28 +507,34 @@ enum RemoveSoftLineBreaksState {
impl RemoveSoftLineBreaksState {
fn should_drop(&mut self, element: &FormatElement) -> bool {
match self {
Self::Default => {
// Entered the start of an `if_group_breaks`
if let FormatElement::Tag(Tag::StartConditionalContent(condition)) = element {
Self::Default => match element {
FormatElement::Line(LineMode::Soft) => true,
// Entered the start of an `if_group_breaks` or `if_group_fits`
// For `if_group_breaks`: Remove the start and end tag and all content in between.
// For `if_group_fits_on_line`: Unwrap the content. This is important because the enclosing group
// might still *expand* if the content exceeds the line width limit, in which case the
// `if_group_fits_on_line` content would be removed.
FormatElement::Tag(Tag::StartConditionalContent(condition)) => {
if condition.mode.is_expanded() {
*self = Self::InIfGroupBreaks {
conditional_content_level: NonZeroUsize::new(1).unwrap(),
};
return true;
}
true
}
false
}
FormatElement::Tag(Tag::EndConditionalContent) => true,
_ => false,
},
Self::InIfGroupBreaks {
conditional_content_level,
} => {
match element {
// A nested `if_group_breaks` or `if_group_fits`
// A nested `if_group_breaks` or `if_group_fits_on_line`
FormatElement::Tag(Tag::StartConditionalContent(_)) => {
*conditional_content_level = conditional_content_level.saturating_add(1);
}
// The end of an `if_group_breaks` or `if_group_fits`.
// The end of an `if_group_breaks` or `if_group_fits_on_line`.
FormatElement::Tag(Tag::EndConditionalContent) => {
if let Some(level) = NonZeroUsize::new(conditional_content_level.get() - 1)
{

View File

@@ -1,6 +1,8 @@
use anyhow::Result;
use std::sync::Arc;
use zip::CompressionMethod;
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{Db, Program, ProgramSettings, PythonVersion, SearchPathSettings};
use ruff_db::files::{File, Files};
use ruff_db::system::{OsSystem, System, SystemPathBuf};
@@ -19,6 +21,7 @@ pub struct ModuleDb {
storage: salsa::Storage<Self>,
files: Files,
system: OsSystem,
rule_selection: Arc<RuleSelection>,
}
impl ModuleDb {
@@ -60,6 +63,7 @@ impl ModuleDb {
storage: self.storage.clone(),
system: self.system.clone(),
files: self.files.snapshot(),
rule_selection: Arc::clone(&self.rule_selection),
}
}
}
@@ -93,6 +97,10 @@ impl Db for ModuleDb {
fn is_file_open(&self, file: File) -> bool {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
}
#[salsa::db]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.8.1"
version = "0.8.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,6 +1,15 @@
from airflow import DAG, dag
from airflow.timetables.simple import NullTimetable
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
from airflow.providers.standard.operators import trigger_dagrun
from airflow.operators.datetime import BranchDateTimeOperator
from airflow.providers.standard.operators import datetime
from airflow.sensors.weekday import DayOfWeekSensor, BranchDayOfWeekOperator
from airflow.providers.standard.sensors import weekday
DAG(dag_id="class_schedule", schedule="@hourly")
DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
@@ -8,6 +17,13 @@ DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
DAG(dag_id="class_timetable", timetable=NullTimetable())
def sla_callback(*arg, **kwargs):
pass
DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback)
@dag(schedule="0 * * * *")
def decorator_schedule():
pass
@@ -21,3 +37,42 @@ def decorator_schedule_interval():
@dag(timetable=NullTimetable())
def decorator_timetable():
pass
@dag(sla_miss_callback=sla_callback)
def decorator_sla_callback():
pass
@dag()
def decorator_deprecated_operator_args():
trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
task_id="trigger_dagrun_op1", execution_date="2024-12-04"
)
trigger_dagrun_op2 = TriggerDagRunOperator(
task_id="trigger_dagrun_op2", execution_date="2024-12-04"
)
branch_dt_op = datetime.BranchDateTimeOperator(
task_id="branch_dt_op", use_task_execution_day=True
)
branch_dt_op2 = BranchDateTimeOperator(
task_id="branch_dt_op2", use_task_execution_day=True
)
dof_task_sensor = weekday.DayOfWeekSensor(
task_id="dof_task_sensor", use_task_execution_day=True
)
dof_task_sensor2 = DayOfWeekSensor(
task_id="dof_task_sensor2", use_task_execution_day=True
)
bdow_op = weekday.BranchDayOfWeekOperator(
task_id="bdow_op", use_task_execution_day=True
)
bdow_op2 = BranchDayOfWeekOperator(task_id="bdow_op2", use_task_execution_day=True)
trigger_dagrun_op >> trigger_dagrun_op2
branch_dt_op >> branch_dt_op2
dof_task_sensor >> dof_task_sensor2
bdow_op >> bdow_op2

View File

@@ -1,8 +1,35 @@
from airflow import PY36, PY37, PY38, PY39, PY310, PY311, PY312
from airflow.triggers.external_task import TaskStateTrigger
from airflow.www.auth import has_access
from airflow.api_connexion.security import requires_access
from airflow.configuration import (
get,
getboolean,
getfloat,
getint,
has_option,
remove_option,
as_dict,
set,
)
from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.metrics.validators import AllowListValidator
from airflow.metrics.validators import BlockListValidator
from airflow.operators.subdag import SubDagOperator
from airflow.sensors.external_task import ExternalTaskSensorLink
from airflow.operators.bash_operator import BashOperator
from airflow.operators.branch_operator import BaseBranchOperator
from airflow.operators.dummy import EmptyOperator, DummyOperator
from airflow.operators import dummy_operator
from airflow.operators.email_operator import EmailOperator
from airflow.sensors.base_sensor_operator import BaseSensorOperator
from airflow.sensors.date_time_sensor import DateTimeSensor
from airflow.sensors.external_task_sensor import (
ExternalTaskMarker,
ExternalTaskSensor,
ExternalTaskSensorLink,
)
from airflow.sensors.time_delta_sensor import TimeDeltaSensor
from airflow.secrets.local_filesystem import get_connection, load_connections
from airflow.utils import dates
from airflow.utils.dates import (
date_range,
@@ -13,20 +40,27 @@ from airflow.utils.dates import (
round_time,
scale_time_units,
)
from airflow.utils.decorators import apply_defaults
from airflow.utils.file import TemporaryDirectory, mkdirs
from airflow.utils.helpers import chain, cross_downstream
from airflow.utils.state import SHUTDOWN, terminating_states
from airflow.utils.dag_cycle_tester import test_cycle
from airflow.utils.trigger_rule import TriggerRule
from airflow.www.auth import has_access
from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
PY36, PY37, PY38, PY39, PY310, PY311, PY312
AWSAthenaHook
TaskStateTrigger
has_access
requires_access
AllowListValidator
BlockListValidator
SubDagOperator
dates.date_range
dates.days_ago
@@ -42,11 +76,38 @@ infer_time_unit
datetime_to_nano
dates.datetime_to_nano
get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
get_connection, load_connections
ExternalTaskSensorLink
BashOperator
BaseBranchOperator
EmptyOperator, DummyOperator
dummy_operator.EmptyOperator
dummy_operator.DummyOperator
EmailOperator
BaseSensorOperator
DateTimeSensor
(ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
TimeDeltaSensor
apply_defaults
TemporaryDirectory
mkdirs
chain
cross_downstream
SHUTDOWN
terminating_states
TriggerRule.DUMMY
TriggerRule.NONE_FAILED_OR_SKIPPED
test_cycle
has_access
get_sensitive_variables_fields, should_hide_value_for_key

View File

@@ -151,6 +151,6 @@ PRESELECT *
FROM {var}.table
"""
# to be handled seperately
# to be handled separately
# query58 = f"SELECT\
# * FROM {var}.table"

View File

@@ -1,6 +1,6 @@
"""
Should emit:
B025 - on lines 15, 22, 31
B025 - on lines 15, 22, 31, 40, 47, 56
"""
import pickle
@@ -36,3 +36,28 @@ except ValueError:
a = 2
except (OSError, TypeError):
a = 2
try:
a = 1
except* ValueError:
a = 2
except* ValueError:
a = 2
try:
a = 1
except* pickle.PickleError:
a = 2
except* ValueError:
a = 2
except* pickle.PickleError:
a = 2
try:
a = 1
except* (ValueError, TypeError):
a = 2
except* ValueError:
a = 2
except* (OSError, TypeError):
a = 2

View File

@@ -9,3 +9,9 @@ warnings.warn(DeprecationWarning("test"))
warnings.warn(DeprecationWarning("test"), source=None)
warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2)
warnings.warn(DeprecationWarning("test"), stacklevel=1)
warnings.warn(
DeprecationWarning("test"),
# some comments here
source = None # no trailing comma
)

View File

@@ -1,6 +1,6 @@
"""
Should emit:
B029 - on lines 8 and 13
B029 - on lines 8, 13, 18 and 23
"""
try:
@@ -11,4 +11,14 @@ except ():
try:
pass
except () as e:
pass
pass
try:
pass
except* ():
pass
try:
pass
except* () as e:
pass

View File

@@ -130,3 +130,23 @@ try:
pass
except (a, b) * (c, d): # B030
pass
try:
pass
except* 1: # Error
pass
try:
pass
except* (1, ValueError): # Error
pass
try:
pass
except* (ValueError, (RuntimeError, (KeyError, TypeError))): # Error
pass
try:
pass
except* (a, b) * (c, d): # B030
pass

View File

@@ -1,6 +1,6 @@
"""
Should emit:
B904 - on lines 10, 11, 16, 62, and 64
B904 - on lines 10, 11, 16, 62, 64, 79, 81, 87, 88, 93 and 97
"""
try:
@@ -71,3 +71,29 @@ except Exception as e:
match 0:
case 0:
raise RuntimeError("boom!")
try:
...
except* Exception as e:
if ...:
raise RuntimeError("boom!")
else:
raise RuntimeError("bang!")
try:
raise ValueError
except* ValueError:
if "abc":
raise TypeError
raise UserWarning
except* AssertionError:
raise # Bare `raise` should not be an error
except* Exception as err:
assert err
raise Exception("No cause here...")
except* BaseException as err:
raise err
except* BaseException as err:
raise some_other_err
finally:
raise Exception("Nothing to chain from, so no warning here")

View File

@@ -47,3 +47,13 @@ dict(map(lambda k, v: (k, v), keys, values))
map(lambda x: x, y if y else z)
map(lambda x: x, (y if y else z))
map(lambda x: x, (x, y, z))
# See https://github.com/astral-sh/ruff/issues/14808
# The following should be Ok since
# named expressions are a syntax error inside comprehensions
a = [1, 2, 3]
b = map(lambda x: x, c := a)
print(c)
# Check nested as well
map(lambda x:x, [c:=a])

View File

@@ -67,5 +67,16 @@ def not_a_deprecated_function() -> None: ...
fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053
from typing import TypeAlias, Literal, Annotated
# see https://github.com/astral-sh/ruff/issues/12995
def foo(bar: typing.Literal["a", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]):...
def foo(bar: typing.Literal["a", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]):...
# Ok
def f(x: int) -> "AnnotationsForClassesWithVeryLongNamesInQuotesAsReturnTypes":...
# Ok
x: TypeAlias = Literal["fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooO"]
# Ok
y: TypeAlias = Annotated[int, "metadataaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]

View File

@@ -81,3 +81,78 @@ def test_not_decorator(param1, param2):
@pytest.mark.parametrize(argnames=("param1,param2"), argvalues=[(1, 2), (3, 4)])
def test_keyword_arguments(param1, param2):
...
@pytest.mark.parametrize(("param",), [(1,), (2,)])
def test_single_element_tuple(param):
...
@pytest.mark.parametrize(("param",), [[1], [2]])
def test_single_element_list(param):
...
@pytest.mark.parametrize(("param",), [[1], [2]])
def test_single_element_list(param):
...
# Unsafe fix
@pytest.mark.parametrize(
(
# comment
"param",
),
[[1], [2]],
)
def test_comment_in_argnames(param):
...
# Unsafe fix
@pytest.mark.parametrize(
("param",),
[
(
# comment
1,
),
(2,),
],
)
def test_comment_in_argvalues(param):
...
# Safe fix
@pytest.mark.parametrize(
("param",),
[
(1,),
# comment
(2,),
],
)
def test_comment_between_argvalues_items(param):
...
# A fix should be suggested for `argnames`, but not for `argvalues`.
@pytest.mark.parametrize(
("param",),
[
(1,),
(2, 3),
],
)
def test_invalid_argvalues(param):
"""
pytest throws the following error for this test:
------------------------------------------------
a.py::test_comment_between_argvalues_items: in "parametrize" the number of names (1):
('param',)
must be equal to the number of values (2):
(2, 3)
------------------------------------------------
"""
...

View File

@@ -0,0 +1,5 @@
import pytest
@pytest.mark.parametrize(("param",), [[1], [2]])
def test_PT006_and_PT007_do_not_conflict(param):
...

View File

@@ -0,0 +1,99 @@
from pathlib import (
Path,
PosixPath,
PurePath,
PurePosixPath,
PureWindowsPath,
WindowsPath,
)
import pathlib
path = Path()
posix_path: pathlib.PosixPath = PosixPath()
pure_path: PurePath = PurePath()
pure_posix_path = pathlib.PurePosixPath()
pure_windows_path: PureWindowsPath = pathlib.PureWindowsPath()
windows_path: pathlib.WindowsPath = pathlib.WindowsPath()
### Errors
path.with_suffix("py")
path.with_suffix(r"s")
path.with_suffix(u'' "json")
path.with_suffix(suffix="js")
posix_path.with_suffix("py")
posix_path.with_suffix(r"s")
posix_path.with_suffix(u'' "json")
posix_path.with_suffix(suffix="js")
pure_path.with_suffix("py")
pure_path.with_suffix(r"s")
pure_path.with_suffix(u'' "json")
pure_path.with_suffix(suffix="js")
pure_posix_path.with_suffix("py")
pure_posix_path.with_suffix(r"s")
pure_posix_path.with_suffix(u'' "json")
pure_posix_path.with_suffix(suffix="js")
pure_windows_path.with_suffix("py")
pure_windows_path.with_suffix(r"s")
pure_windows_path.with_suffix(u'' "json")
pure_windows_path.with_suffix(suffix="js")
windows_path.with_suffix("py")
windows_path.with_suffix(r"s")
windows_path.with_suffix(u'' "json")
windows_path.with_suffix(suffix="js")
### No errors
path.with_suffix()
path.with_suffix('')
path.with_suffix(".py")
path.with_suffix("foo", "bar")
path.with_suffix(suffix)
path.with_suffix(f"oo")
path.with_suffix(b"ar")
posix_path.with_suffix()
posix_path.with_suffix('')
posix_path.with_suffix(".py")
posix_path.with_suffix("foo", "bar")
posix_path.with_suffix(suffix)
posix_path.with_suffix(f"oo")
posix_path.with_suffix(b"ar")
pure_path.with_suffix()
pure_path.with_suffix('')
pure_path.with_suffix(".py")
pure_path.with_suffix("foo", "bar")
pure_path.with_suffix(suffix)
pure_path.with_suffix(f"oo")
pure_path.with_suffix(b"ar")
pure_posix_path.with_suffix()
pure_posix_path.with_suffix('')
pure_posix_path.with_suffix(".py")
pure_posix_path.with_suffix("foo", "bar")
pure_posix_path.with_suffix(suffix)
pure_posix_path.with_suffix(f"oo")
pure_posix_path.with_suffix(b"ar")
pure_windows_path.with_suffix()
pure_windows_path.with_suffix('')
pure_windows_path.with_suffix(".py")
pure_windows_path.with_suffix("foo", "bar")
pure_windows_path.with_suffix(suffix)
pure_windows_path.with_suffix(f"oo")
pure_windows_path.with_suffix(b"ar")
windows_path.with_suffix()
windows_path.with_suffix('')
windows_path.with_suffix(".py")
windows_path.with_suffix("foo", "bar")
windows_path.with_suffix(suffix)
windows_path.with_suffix(f"oo")
windows_path.with_suffix(b"ar")

View File

@@ -0,0 +1,110 @@
from pathlib import (
Path,
PosixPath,
PurePath,
PurePosixPath,
PureWindowsPath,
WindowsPath,
)
def test_path(p: Path) -> None:
## Errors
p.with_suffix("py")
p.with_suffix(r"s")
p.with_suffix(u'' "json")
p.with_suffix(suffix="js")
## No errors
p.with_suffix()
p.with_suffix('')
p.with_suffix(".py")
p.with_suffix("foo", "bar")
p.with_suffix(suffix)
p.with_suffix(f"oo")
p.with_suffix(b"ar")
def test_posix_path(p: PosixPath) -> None:
## Errors
p.with_suffix("py")
p.with_suffix(r"s")
p.with_suffix(u'' "json")
p.with_suffix(suffix="js")
## No errors
p.with_suffix()
p.with_suffix('')
p.with_suffix(".py")
p.with_suffix("foo", "bar")
p.with_suffix(suffix)
p.with_suffix(f"oo")
p.with_suffix(b"ar")
def test_pure_path(p: PurePath) -> None:
## Errors
p.with_suffix("py")
p.with_suffix(r"s")
p.with_suffix(u'' "json")
p.with_suffix(suffix="js")
## No errors
p.with_suffix()
p.with_suffix('')
p.with_suffix(".py")
p.with_suffix("foo", "bar")
p.with_suffix(suffix)
p.with_suffix(f"oo")
p.with_suffix(b"ar")
def test_pure_posix_path(p: PurePosixPath) -> None:
## Errors
p.with_suffix("py")
p.with_suffix(r"s")
p.with_suffix(u'' "json")
p.with_suffix(suffix="js")
## No errors
p.with_suffix()
p.with_suffix('')
p.with_suffix(".py")
p.with_suffix("foo", "bar")
p.with_suffix(suffix)
p.with_suffix(f"oo")
p.with_suffix(b"ar")
def test_pure_windows_path(p: PureWindowsPath) -> None:
## Errors
p.with_suffix("py")
p.with_suffix(r"s")
p.with_suffix(u'' "json")
p.with_suffix(suffix="js")
## No errors
p.with_suffix()
p.with_suffix('')
p.with_suffix(".py")
p.with_suffix("foo", "bar")
p.with_suffix(suffix)
p.with_suffix(f"oo")
p.with_suffix(b"ar")
def test_windows_path(p: WindowsPath) -> None:
## Errors
p.with_suffix("py")
p.with_suffix(r"s")
p.with_suffix(u'' "json")
p.with_suffix(suffix="js")
## No errors
p.with_suffix()
p.with_suffix('')
p.with_suffix(".py")
p.with_suffix("foo", "bar")
p.with_suffix(suffix)
p.with_suffix(f"oo")
p.with_suffix(b"ar")

View File

@@ -178,7 +178,7 @@ def foo(x: int) -> int | None:
"""A very helpful docstring.
Args:
x (int): An interger.
x (int): An integer.
"""
if x < 0:
return None
@@ -191,7 +191,7 @@ def foo(x):
"""A very helpful docstring.
Args:
x (int): An interger.
x (int): An integer.
"""
if x < 0:
return None

View File

@@ -154,7 +154,7 @@ def foo(x: int) -> int | None:
Parameters
----------
x : int
An interger.
An integer.
"""
if x < 0:
return None
@@ -169,7 +169,7 @@ def foo(x):
Parameters
----------
x : int
An interger.
An integer.
"""
if x < 0:
return None

View File

@@ -119,10 +119,30 @@ c = int(input())
if a > b and b < c:
pass
# Unfixable due to parentheses.
# fixes will balance parentheses
(a < b) and b < c
a < b and (b < c)
((a < b) and b < c)
(a < b) and (b < c)
(((a < b))) and (b < c)
(a<b) and b<c and ((c<d))
# should error and fix
a<b<c and c<d
# more involved examples (all should error and fix)
a < ( # sneaky comment
b
# more comments
) and b < c
(
a
<b
# hmmm...
<c
and ((c<d))
)
a < (b) and (((b)) < c)

View File

@@ -13,3 +13,5 @@ os.getenv("B", Z)
os.getenv("AA", "GOOD" if Z else "BAR")
os.getenv("AA", 1 if Z else "BAR") # [invalid-envvar-default]
os.environ.get("TEST", 12) # [invalid-envvar-default]
os.environ.get("TEST", "AA" * 12)
os.environ.get("TEST", 13 * "AA")

View File

@@ -41,3 +41,8 @@ MyType = TypedDict("MyType", {})
# Empty dict call
MyType = TypedDict("MyType", dict())
# Unsafe fix if comments are present
X = TypedDict("X", {
"some_config": int, # important
})

View File

@@ -31,3 +31,9 @@ S3File = NamedTuple(
("dataHPK",* str),
],
)
# Unsafe fix if comments are present
X = NamedTuple("X", [
("some_config", int), # important
])

View File

@@ -12,7 +12,7 @@ t.Literal[1, t.Literal[2, t.Literal[1]]]
Literal[
1, # comment 1
Literal[ # another comment
1 # yet annother comment
1 # yet another comment
]
] # once

View File

@@ -12,7 +12,7 @@ t.Literal[1, t.Literal[2, t.Literal[1]]]
Literal[
1, # comment 1
Literal[ # another comment
1 # yet annother comment
1 # yet another comment
]
] # once

View File

@@ -0,0 +1,50 @@
import math
### Safely fixable
# Arguments are not checked
int(id())
int(len([]))
int(ord(foo))
int(hash(foo, bar))
int(int(''))
int(math.comb())
int(math.factorial())
int(math.gcd())
int(math.lcm())
int(math.isqrt())
int(math.perm())
### Unsafe
int(math.ceil())
int(math.floor())
int(math.trunc())
### `round()`
## Errors
int(round(0))
int(round(0, 0))
int(round(0, None))
int(round(0.1))
int(round(0.1, None))
# Argument type is not checked
foo = type("Foo", (), {"__round__": lambda self: 4.2})()
int(round(foo))
int(round(foo, 0))
int(round(foo, None))
## No errors
int(round(0, 3.14))
int(round(0, non_literal))
int(round(0, 0), base)
int(round(0, 0, extra=keyword))
int(round(0.1, 0))

View File

@@ -77,7 +77,7 @@ class Class_:
_var = "method variable" # [RUF052]
return _var
def fun(_var): # [RUF052]
def fun(_var): # parameters are ignored
return _var
def fun():
@@ -129,3 +129,19 @@ def unfixables():
# unfixable because the rename would shadow a variable from the outer function
_local = "local4"
print(_local)
def special_calls():
from typing import TypeVar, ParamSpec, NamedTuple
from enum import Enum
from collections import namedtuple
_P = ParamSpec("_P")
_T = TypeVar(name="_T", covariant=True, bound=int|str)
_NT = NamedTuple("_NT", [("foo", int)])
_E = Enum("_E", ["a", "b", "c"])
_NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
_NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
_DynamicClass = type("_DynamicClass", (), {})
_NotADynamicClass = type("_NotADynamicClass")
print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)

View File

@@ -1093,6 +1093,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::Airflow3Removal) {
airflow::rules::removed_in_3(checker, expr);
}
if checker.enabled(Rule::UnnecessaryCastToInt) {
ruff::rules::unnecessary_cast_to_int(checker, call);
}
if checker.enabled(Rule::DotlessPathlibWithSuffix) {
flake8_use_pathlib::rules::dotless_pathlib_with_suffix(checker, call);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[

View File

@@ -909,6 +909,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "206") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsSepSplit),
(Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob),
(Flake8UsePathlib, "208") => (RuleGroup::Preview, rules::flake8_use_pathlib::violations::OsListdir),
(Flake8UsePathlib, "210") => (RuleGroup::Preview, rules::flake8_use_pathlib::rules::DotlessPathlibWithSuffix),
// flake8-logging-format
(Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat),
@@ -983,6 +984,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern),
(Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
(Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral),
(Ruff, "046") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryCastToInt),
(Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing),
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),

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