Compare commits
76 Commits
cjm/pylint
...
micha/fixp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05854142e7 | ||
|
|
33eb008fb6 | ||
|
|
04d4febfc4 | ||
|
|
d566636ca5 | ||
|
|
51cef5a72b | ||
|
|
2cf5cba7ff | ||
|
|
ce0800fccf | ||
|
|
d03a7069ad | ||
|
|
f5096f2050 | ||
|
|
895b6161a6 | ||
|
|
74fe7982ba | ||
|
|
51386b3c7a | ||
|
|
51e2effd2d | ||
|
|
82d31a6014 | ||
|
|
78054824c0 | ||
|
|
c6f4929cdc | ||
|
|
2ec0d7e072 | ||
|
|
ad658f4d68 | ||
|
|
3dedd70a92 | ||
|
|
fab862c8cd | ||
|
|
0d9b6a0975 | ||
|
|
c5e299e796 | ||
|
|
c504001b32 | ||
|
|
04457f99b6 | ||
|
|
a33d0d4bf4 | ||
|
|
443f62e98d | ||
|
|
a2e9a7732a | ||
|
|
b2de749c32 | ||
|
|
9085f18353 | ||
|
|
8152ba7cb7 | ||
|
|
3d01d3be3e | ||
|
|
4510a236d3 | ||
|
|
76b6d53d8b | ||
|
|
f82b72882b | ||
|
|
d07eefc408 | ||
|
|
f7237e3b69 | ||
|
|
2f9992b6ef | ||
|
|
457ec4dddd | ||
|
|
aa0614509b | ||
|
|
9000eb3bfd | ||
|
|
7f50b503cf | ||
|
|
24d3fc27fb | ||
|
|
6f821ac846 | ||
|
|
d410d12bc5 | ||
|
|
89424cce5f | ||
|
|
fd76d70a31 | ||
|
|
a4c8e43c5f | ||
|
|
ada4c4cb1f | ||
|
|
bb6c7cad07 | ||
|
|
47e3aa40b3 | ||
|
|
9a6633da0b | ||
|
|
de78da5ee6 | ||
|
|
20d64b9c85 | ||
|
|
4850c187ea | ||
|
|
3f32446e16 | ||
|
|
784daae497 | ||
|
|
178c882740 | ||
|
|
101e1a5ddd | ||
|
|
965a4dd731 | ||
|
|
5e2c818417 | ||
|
|
90c12f4177 | ||
|
|
6e9fb9af38 | ||
|
|
a507c1b8b3 | ||
|
|
5a91badb8b | ||
|
|
1945bfdb84 | ||
|
|
a95c73d5d0 | ||
|
|
78b4c3ccf1 | ||
|
|
2485afe640 | ||
|
|
b8ed729f59 | ||
|
|
108c470348 | ||
|
|
87c64c9eab | ||
|
|
a10606dda2 | ||
|
|
d1c6dd9ac1 | ||
|
|
073b993ab0 | ||
|
|
6a36cd6f02 | ||
|
|
3b15af6d4f |
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Report an issue with ty
|
||||
url: https://github.com/astral-sh/ty/issues/new/choose
|
||||
about: Please report issues for our type checker ty in the ty repository.
|
||||
- name: Documentation
|
||||
url: https://docs.astral.sh/ruff
|
||||
about: Please consult the documentation before creating an issue.
|
||||
|
||||
16
.github/workflows/build-binaries.yml
vendored
16
.github/workflows/build-binaries.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: arm64
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: ${{ matrix.platform.arch }}
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -298,7 +298,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
@@ -363,7 +363,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -429,7 +429,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
|
||||
4
.github/workflows/build-docker.yml
vendored
4
.github/workflows/build-docker.yml
vendored
@@ -113,7 +113,7 @@ jobs:
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
|
||||
32
.github/workflows/ci.yaml
vendored
32
.github/workflows/ci.yaml
vendored
@@ -239,11 +239,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -297,11 +297,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -407,11 +407,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -460,7 +460,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
with:
|
||||
@@ -525,11 +525,11 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
name: Download comparison Ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
@@ -649,7 +649,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
name: Download new ty binary
|
||||
id: ty-new
|
||||
with:
|
||||
@@ -705,7 +705,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -759,7 +759,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
@@ -830,12 +830,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
# installation fails on 3.13 and newer
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
name: Download development ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
@@ -908,7 +908,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
|
||||
2
.github/workflows/publish-docs.yml
vendored
2
.github/workflows/publish-docs.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
path: wheels
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4/cargo-dist-installer.sh | sh"
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.5-prerelease.1/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
with:
|
||||
|
||||
@@ -65,7 +65,7 @@ repos:
|
||||
- black==25.1.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.31.1
|
||||
rev: v1.32.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -79,7 +79,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.7
|
||||
rev: v0.11.8
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
@@ -71,8 +71,7 @@ representative at an online or offline event.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
<charlie.r.marsh@gmail.com>.
|
||||
reported to the community leaders responsible for enforcement at <hey@astral.sh>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
@@ -366,6 +366,15 @@ uvx --from ./python/ruff-ecosystem ruff-ecosystem format ruff "./target/debug/ru
|
||||
|
||||
See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/python/ruff-ecosystem) for more details.
|
||||
|
||||
## Upgrading Rust
|
||||
|
||||
1. Change the `channel` in `./rust-toolchain.toml` to the new Rust version (`<latest>`)
|
||||
1. Change the `rust-version` in the `./Cargo.toml` to `<latest> - 2` (e.g. 1.84 if the latest is 1.86)
|
||||
1. Run `cargo clippy --fix --allow-dirty --allow-staged` to fix new clippy warnings
|
||||
1. Create and merge the PR
|
||||
1. Bump the Rust version in Ruff's conda forge recipe. See [this PR](https://github.com/conda-forge/ruff-feedstock/pull/266) for an example.
|
||||
1. Enjoy the new Rust version!
|
||||
|
||||
## Benchmarking and Profiling
|
||||
|
||||
We have several ways of benchmarking and profiling Ruff:
|
||||
|
||||
81
Cargo.lock
generated
81
Cargo.lock
generated
@@ -150,9 +150,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "assert_fs"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674"
|
||||
checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"doc-comment",
|
||||
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -487,7 +487,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -904,7 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1117,9 +1117,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
@@ -1132,7 +1132,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.15.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1361,7 +1361,7 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.15.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1381,7 +1381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.15.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1485,7 +1485,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1539,9 +1539,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.10"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6"
|
||||
checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
@@ -1549,14 +1549,14 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.10"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254"
|
||||
checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2834,6 +2834,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
@@ -3205,7 +3206,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3218,7 +3219,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.3",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3235,14 +3236,14 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.21.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
"crossbeam-queue",
|
||||
"dashmap 6.1.0",
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.15.3",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"parking_lot",
|
||||
@@ -3258,13 +3259,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.21.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.21.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3604,7 +3605,7 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix 1.0.2",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3799,9 +3800,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.20"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
|
||||
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -3811,26 +3812,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.24"
|
||||
version = "0.22.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
@@ -3947,6 +3955,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
"clap",
|
||||
"clap_complete_command",
|
||||
"colored 3.0.0",
|
||||
"countme",
|
||||
"crossbeam",
|
||||
@@ -4030,7 +4039,7 @@ dependencies = [
|
||||
"countme",
|
||||
"dir-test",
|
||||
"drop_bomb",
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.15.3",
|
||||
"indexmap",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
@@ -4586,7 +4595,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4824,9 +4833,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.4"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
|
||||
checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
74
Cargo.toml
74
Cargo.toml
@@ -124,7 +124,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "ef93d3830ffb8dc621fef1ba7b234ccef9dc3639" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -268,75 +268,3 @@ debug = 1
|
||||
# The profile that 'cargo dist' will build with.
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
# Config for 'dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.28.4"
|
||||
# Make distability of apps opt-in instead of opt-out
|
||||
dist = false
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
installers = ["shell", "powershell"]
|
||||
# The archive format to use for windows builds (defaults .zip)
|
||||
windows-archive = ".zip"
|
||||
# The archive format to use for non-windows builds (defaults .tar.xz)
|
||||
unix-archive = ".tar.gz"
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"arm-unknown-linux-musleabihf",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"armv7-unknown-linux-musleabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"i686-unknown-linux-gnu",
|
||||
"i686-unknown-linux-musl",
|
||||
"powerpc64-unknown-linux-gnu",
|
||||
"powerpc64le-unknown-linux-gnu",
|
||||
"s390x-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
]
|
||||
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
|
||||
auto-includes = false
|
||||
# Whether dist should create a Github Release or use an existing draft
|
||||
create-release = true
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "plan"
|
||||
# Whether CI should trigger releases with dispatches instead of tag pushes
|
||||
dispatch-releases = true
|
||||
# Which phase dist should use to create the GitHub release
|
||||
github-release = "announce"
|
||||
# Whether CI should include auto-generated code to build local artifacts
|
||||
build-local-artifacts = false
|
||||
# Local artifacts jobs to run in CI
|
||||
local-artifacts-jobs = ["./build-binaries", "./build-docker"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# Post-announce jobs to run in CI
|
||||
post-announce-jobs = [
|
||||
"./notify-dependents",
|
||||
"./publish-docs",
|
||||
"./publish-playground",
|
||||
]
|
||||
# Custom permissions for GitHub Jobs
|
||||
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
|
||||
# Whether to install an updater program
|
||||
install-updater = false
|
||||
# Path that installers should place binaries in
|
||||
install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
|
||||
|
||||
[workspace.metadata.dist.github-custom-runners]
|
||||
global = "depot-ubuntu-latest-4"
|
||||
|
||||
[workspace.metadata.dist.github-action-commits]
|
||||
"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4
|
||||
"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2
|
||||
"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0
|
||||
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3
|
||||
|
||||
@@ -616,7 +616,7 @@ mod tests {
|
||||
let settings = Settings {
|
||||
cache_dir,
|
||||
linter: LinterSettings {
|
||||
unresolved_target_version: PythonVersion::latest(),
|
||||
unresolved_target_version: PythonVersion::latest().into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::default()
|
||||
|
||||
@@ -3930,7 +3930,7 @@ from typing import Union;foo: Union[int, str] = 1
|
||||
linter.per_file_ignores = {}
|
||||
linter.safety_table.forced_safe = []
|
||||
linter.safety_table.forced_unsafe = []
|
||||
linter.unresolved_target_version = 3.9
|
||||
linter.unresolved_target_version = none
|
||||
linter.per_file_target_version = {}
|
||||
linter.preview = disabled
|
||||
linter.explicit_preview_rules = false
|
||||
@@ -4215,7 +4215,7 @@ from typing import Union;foo: Union[int, str] = 1
|
||||
linter.per_file_ignores = {}
|
||||
linter.safety_table.forced_safe = []
|
||||
linter.safety_table.forced_unsafe = []
|
||||
linter.unresolved_target_version = 3.9
|
||||
linter.unresolved_target_version = none
|
||||
linter.per_file_target_version = {}
|
||||
linter.preview = disabled
|
||||
linter.explicit_preview_rules = false
|
||||
|
||||
@@ -5,7 +5,6 @@ info:
|
||||
args:
|
||||
- rule
|
||||
- F401
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -84,6 +83,11 @@ else:
|
||||
print("numpy is not installed")
|
||||
```
|
||||
|
||||
## Preview
|
||||
When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
|
||||
the criterion for determining whether an import is first-party
|
||||
is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
|
||||
|
||||
## Options
|
||||
- `lint.ignore-init-module-imports`
|
||||
- `lint.pyflakes.allowed-unused-imports`
|
||||
|
||||
@@ -232,6 +232,15 @@ impl Diagnostic {
|
||||
pub fn primary_tags(&self) -> Option<&[DiagnosticTag]> {
|
||||
self.primary_annotation().map(|ann| ann.tags.as_slice())
|
||||
}
|
||||
|
||||
/// Returns a key that can be used to sort two diagnostics into the canonical order
|
||||
/// in which they should appear when rendered.
|
||||
pub fn rendering_sort_key<'a>(&'a self, db: &'a dyn Db) -> impl Ord + 'a {
|
||||
RenderingSortKey {
|
||||
db,
|
||||
diagnostic: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
@@ -243,6 +252,64 @@ struct DiagnosticInner {
|
||||
subs: Vec<SubDiagnostic>,
|
||||
}
|
||||
|
||||
struct RenderingSortKey<'a> {
|
||||
db: &'a dyn Db,
|
||||
diagnostic: &'a Diagnostic,
|
||||
}
|
||||
|
||||
impl Ord for RenderingSortKey<'_> {
|
||||
// We sort diagnostics in a way that keeps them in source order
|
||||
// and grouped by file. After that, we fall back to severity
|
||||
// (with fatal messages sorting before info messages) and then
|
||||
// finally the diagnostic ID.
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
if let (Some(span1), Some(span2)) = (
|
||||
self.diagnostic.primary_span(),
|
||||
other.diagnostic.primary_span(),
|
||||
) {
|
||||
let order = span1
|
||||
.file()
|
||||
.path(self.db)
|
||||
.as_str()
|
||||
.cmp(span2.file().path(self.db).as_str());
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
|
||||
if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) {
|
||||
let order = range1.start().cmp(&range2.start());
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reverse so that, e.g., Fatal sorts before Info.
|
||||
let order = self
|
||||
.diagnostic
|
||||
.severity()
|
||||
.cmp(&other.diagnostic.severity())
|
||||
.reverse();
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
self.diagnostic.id().cmp(&other.diagnostic.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for RenderingSortKey<'_> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RenderingSortKey<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RenderingSortKey<'_> {}
|
||||
|
||||
/// A collection of information subservient to a diagnostic.
|
||||
///
|
||||
/// A sub-diagnostic is always rendered after the parent diagnostic it is
|
||||
@@ -674,6 +741,10 @@ impl Severity {
|
||||
Severity::Fatal => AnnotateLevel::Error,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_fatal(self) -> bool {
|
||||
matches!(self, Severity::Fatal)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for rendering diagnostics.
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn max_parallelism() -> NonZeroUsize {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::files::Files;
|
||||
use crate::system::TestSystem;
|
||||
@@ -69,6 +69,8 @@ mod tests {
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
use crate::Db;
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
/// Database that can be used for testing.
|
||||
///
|
||||
/// Uses an in memory filesystem and it stubs out the vendored files by default.
|
||||
@@ -79,36 +81,37 @@ mod tests {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Events,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let events = Events::default();
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
tracing::trace!("event: {:?}", event);
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: VendoredFileSystem::default(),
|
||||
events: std::sync::Arc::default(),
|
||||
events,
|
||||
files: Files::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Empties the internal store of salsa events that have been emitted,
|
||||
/// and returns them as a `Vec` (equivalent to [`std::mem::take`]).
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are pending database snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events)
|
||||
.expect("expected no pending salsa database snapshots.");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
std::mem::take(inner.get_mut().unwrap())
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the emitted salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are pending database snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
@@ -148,12 +151,5 @@ mod tests {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {:?}", event);
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,4 @@ impl Db for ModuleDb {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for ModuleDb {
|
||||
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
|
||||
}
|
||||
impl salsa::Database for ModuleDb {}
|
||||
|
||||
@@ -76,6 +76,7 @@ insta = { workspace = true, features = ["filters", "json", "redactions"] }
|
||||
test-case = { workspace = true }
|
||||
# Disable colored output in tests
|
||||
colored = { workspace = true, features = ["no-color"] }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -6,3 +6,12 @@ except ModuleNotFoundError:
|
||||
from airflow.datasets import Dataset as Asset
|
||||
|
||||
Asset
|
||||
|
||||
try:
|
||||
from airflow.sdk import Asset
|
||||
except ModuleNotFoundError:
|
||||
from airflow import datasets
|
||||
|
||||
Asset = datasets.Dataset
|
||||
|
||||
asset = Asset()
|
||||
|
||||
@@ -39,3 +39,9 @@ run(c := "true")
|
||||
# But non-instant are not.
|
||||
(e := "echo")
|
||||
run(e)
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17798
|
||||
# Tuple literals are trusted
|
||||
check_output(("literal", "cmd", "using", "tuple"), text=True)
|
||||
Popen(("literal", "cmd", "using", "tuple"))
|
||||
|
||||
@@ -84,3 +84,9 @@ str(
|
||||
'''Lorem
|
||||
ipsum''' # Comment
|
||||
).foo
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17606
|
||||
bool(True)and None
|
||||
int(1)and None
|
||||
float(1.)and None
|
||||
bool(True)and()
|
||||
|
||||
37
crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py
vendored
Normal file
37
crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# Errors
|
||||
1 in []
|
||||
1 not in []
|
||||
2 in list()
|
||||
2 not in list()
|
||||
_ in ()
|
||||
_ not in ()
|
||||
'x' in tuple()
|
||||
'y' not in tuple()
|
||||
'a' in set()
|
||||
'a' not in set()
|
||||
'b' in {}
|
||||
'b' not in {}
|
||||
1 in dict()
|
||||
2 not in dict()
|
||||
"a" in ""
|
||||
b'c' in b""
|
||||
"b" in f""
|
||||
b"a" in bytearray()
|
||||
b"a" in bytes()
|
||||
1 in frozenset()
|
||||
|
||||
# OK
|
||||
1 in [2]
|
||||
1 in [1, 2, 3]
|
||||
_ in ('a')
|
||||
_ not in ('a')
|
||||
'a' in set('a', 'b')
|
||||
'a' not in set('b', 'c')
|
||||
'b' in {1: 2}
|
||||
'b' not in {3: 4}
|
||||
"a" in "x"
|
||||
b'c' in b"x"
|
||||
"b" in f"x"
|
||||
b"a" in bytearray([2])
|
||||
b"a" in bytes("a", "utf-8")
|
||||
1 in frozenset("c")
|
||||
11
crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py
vendored
Normal file
11
crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
async def elements(n):
|
||||
yield n
|
||||
|
||||
def regular_function():
|
||||
[x async for x in elements(1)]
|
||||
|
||||
async with elements(1) as x:
|
||||
pass
|
||||
|
||||
async for _ in elements(1):
|
||||
pass
|
||||
5
crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
def func():
|
||||
await 1
|
||||
|
||||
# Top-level await
|
||||
await 1
|
||||
@@ -1504,6 +1504,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.enabled(Rule::NanComparison) {
|
||||
pylint::rules::nan_comparison(checker, left, comparators);
|
||||
}
|
||||
if checker.enabled(Rule::InEmptyCollection) {
|
||||
ruff::rules::in_empty_collection(checker, compare);
|
||||
}
|
||||
if checker.enabled(Rule::InDictKeys) {
|
||||
flake8_simplify::rules::key_in_dict_compare(checker, compare);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::StmtFunctionDef;
|
||||
use ruff_python_ast::{PythonVersion, StmtFunctionDef};
|
||||
use ruff_python_semantic::{ScopeKind, SemanticModel};
|
||||
|
||||
use crate::rules::flake8_type_checking;
|
||||
@@ -29,7 +29,11 @@ pub(super) enum AnnotationContext {
|
||||
impl AnnotationContext {
|
||||
/// Determine the [`AnnotationContext`] for an annotation based on the current scope of the
|
||||
/// semantic model.
|
||||
pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self {
|
||||
pub(super) fn from_model(
|
||||
semantic: &SemanticModel,
|
||||
settings: &LinterSettings,
|
||||
version: PythonVersion,
|
||||
) -> Self {
|
||||
// If the annotation is in a class scope (e.g., an annotated assignment for a
|
||||
// class field) or a function scope, and that class or function is marked as
|
||||
// runtime-required, treat the annotation as runtime-required.
|
||||
@@ -59,7 +63,7 @@ impl AnnotationContext {
|
||||
// If `__future__` annotations are enabled or it's a stub file,
|
||||
// then annotations are never evaluated at runtime,
|
||||
// so we can treat them as typing-only.
|
||||
if semantic.future_annotations_or_stub() {
|
||||
if semantic.future_annotations_or_stub() || version.defers_annotations() {
|
||||
return Self::TypingOnly;
|
||||
}
|
||||
|
||||
@@ -81,6 +85,7 @@ impl AnnotationContext {
|
||||
function_def: &StmtFunctionDef,
|
||||
semantic: &SemanticModel,
|
||||
settings: &LinterSettings,
|
||||
version: PythonVersion,
|
||||
) -> Self {
|
||||
if flake8_type_checking::helpers::runtime_required_function(
|
||||
function_def,
|
||||
@@ -88,7 +93,7 @@ impl AnnotationContext {
|
||||
semantic,
|
||||
) {
|
||||
Self::RuntimeRequired
|
||||
} else if semantic.future_annotations_or_stub() {
|
||||
} else if semantic.future_annotations_or_stub() || version.defers_annotations() {
|
||||
Self::TypingOnly
|
||||
} else {
|
||||
Self::RuntimeEvaluated
|
||||
|
||||
@@ -72,7 +72,7 @@ use crate::rules::pyflakes::rules::{
|
||||
};
|
||||
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
|
||||
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
|
||||
use crate::settings::{flags, LinterSettings};
|
||||
use crate::settings::{flags, LinterSettings, TargetVersion};
|
||||
use crate::{docstrings, noqa, Locator};
|
||||
|
||||
mod analyze;
|
||||
@@ -232,7 +232,7 @@ pub(crate) struct Checker<'a> {
|
||||
/// A state describing if a docstring is expected or not.
|
||||
docstring_state: DocstringState,
|
||||
/// The target [`PythonVersion`] for version-dependent checks.
|
||||
target_version: PythonVersion,
|
||||
target_version: TargetVersion,
|
||||
/// Helper visitor for detecting semantic syntax errors.
|
||||
#[expect(clippy::struct_field_names)]
|
||||
semantic_checker: SemanticSyntaxChecker,
|
||||
@@ -257,7 +257,7 @@ impl<'a> Checker<'a> {
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&'a CellOffsets>,
|
||||
notebook_index: Option<&'a NotebookIndex>,
|
||||
target_version: PythonVersion,
|
||||
target_version: TargetVersion,
|
||||
) -> Checker<'a> {
|
||||
let semantic = SemanticModel::new(&settings.typing_modules, path, module);
|
||||
Self {
|
||||
@@ -523,9 +523,16 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`PythonVersion`] to use for version-related checks.
|
||||
pub(crate) const fn target_version(&self) -> PythonVersion {
|
||||
self.target_version
|
||||
/// Return the [`PythonVersion`] to use for version-related lint rules.
|
||||
///
|
||||
/// If the user did not provide a target version, this defaults to the lowest supported Python
|
||||
/// version ([`PythonVersion::default`]).
|
||||
///
|
||||
/// Note that this method should not be used for version-related syntax errors emitted by the
|
||||
/// parser or the [`SemanticSyntaxChecker`], which should instead default to the _latest_
|
||||
/// supported Python version.
|
||||
pub(crate) fn target_version(&self) -> PythonVersion {
|
||||
self.target_version.linter_version()
|
||||
}
|
||||
|
||||
fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Checker)) {
|
||||
@@ -583,7 +590,10 @@ impl TypingImporter<'_, '_> {
|
||||
|
||||
impl SemanticSyntaxContext for Checker<'_> {
|
||||
fn python_version(&self) -> PythonVersion {
|
||||
self.target_version
|
||||
// Reuse `parser_version` here, which should default to `PythonVersion::latest` instead of
|
||||
// `PythonVersion::default` to minimize version-related semantic syntax errors when
|
||||
// `target_version` is unset.
|
||||
self.target_version.parser_version()
|
||||
}
|
||||
|
||||
fn global(&self, name: &str) -> Option<TextRange> {
|
||||
@@ -1004,9 +1014,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
}
|
||||
|
||||
// Function annotations are always evaluated at runtime, unless future annotations
|
||||
// are enabled.
|
||||
let annotation =
|
||||
AnnotationContext::from_function(function_def, &self.semantic, self.settings);
|
||||
// are enabled or the Python version is at least 3.14.
|
||||
let annotation = AnnotationContext::from_function(
|
||||
function_def,
|
||||
&self.semantic,
|
||||
self.settings,
|
||||
self.target_version(),
|
||||
);
|
||||
|
||||
// The first parameter may be a single dispatch.
|
||||
let singledispatch =
|
||||
@@ -1203,7 +1217,11 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
value,
|
||||
..
|
||||
}) => {
|
||||
match AnnotationContext::from_model(&self.semantic, self.settings) {
|
||||
match AnnotationContext::from_model(
|
||||
&self.semantic,
|
||||
self.settings,
|
||||
self.target_version(),
|
||||
) {
|
||||
AnnotationContext::RuntimeRequired => {
|
||||
self.visit_runtime_required_annotation(annotation);
|
||||
}
|
||||
@@ -1358,7 +1376,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
// we can't defer again, or we'll infinitely recurse!
|
||||
&& !self.semantic.in_deferred_type_definition()
|
||||
&& self.semantic.in_type_definition()
|
||||
&& self.semantic.future_annotations_or_stub()
|
||||
&& (self.semantic.future_annotations_or_stub()||self.target_version().defers_annotations())
|
||||
&& (self.semantic.in_annotation() || self.source_type.is_stub())
|
||||
{
|
||||
if let Expr::StringLiteral(string_literal) = expr {
|
||||
@@ -2585,7 +2603,8 @@ impl<'a> Checker<'a> {
|
||||
// if they are annotations in a module where `from __future__ import
|
||||
// annotations` is active, or they are type definitions in a stub file.
|
||||
debug_assert!(
|
||||
self.semantic.future_annotations_or_stub()
|
||||
(self.semantic.future_annotations_or_stub()
|
||||
|| self.target_version().defers_annotations())
|
||||
&& (self.source_type.is_stub() || self.semantic.in_annotation())
|
||||
);
|
||||
|
||||
@@ -2923,7 +2942,7 @@ pub(crate) fn check_ast(
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
notebook_index: Option<&NotebookIndex>,
|
||||
target_version: PythonVersion,
|
||||
target_version: TargetVersion,
|
||||
) -> (Vec<Diagnostic>, Vec<SemanticSyntaxError>) {
|
||||
let module_path = package
|
||||
.map(PackageRoot::path)
|
||||
|
||||
@@ -1014,6 +1014,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),
|
||||
(Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip),
|
||||
(Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable),
|
||||
(Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
||||
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::registry::{AsRule, Rule, RuleSet};
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES};
|
||||
use crate::settings::types::UnsafeFixes;
|
||||
use crate::settings::{flags, LinterSettings};
|
||||
use crate::settings::{flags, LinterSettings, TargetVersion};
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::{directives, fs, warn_user_once, Locator};
|
||||
|
||||
@@ -111,7 +111,7 @@ pub fn check_path(
|
||||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
parsed: &Parsed<ModModule>,
|
||||
target_version: PythonVersion,
|
||||
target_version: TargetVersion,
|
||||
) -> Vec<Message> {
|
||||
// Aggregate all diagnostics.
|
||||
let mut diagnostics = vec![];
|
||||
@@ -160,7 +160,7 @@ pub fn check_path(
|
||||
locator,
|
||||
comment_ranges,
|
||||
settings,
|
||||
target_version,
|
||||
target_version.linter_version(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ pub fn check_path(
|
||||
package,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
target_version,
|
||||
target_version.linter_version(),
|
||||
);
|
||||
|
||||
diagnostics.extend(import_diagnostics);
|
||||
@@ -390,7 +390,7 @@ pub fn add_noqa_to_path(
|
||||
) -> Result<usize> {
|
||||
// Parse once.
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
let parsed = parse_unchecked_source(source_kind, source_type, target_version);
|
||||
let parsed = parse_unchecked_source(source_kind, source_type, target_version.parser_version());
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::new(source_kind.source_code());
|
||||
@@ -451,11 +451,13 @@ pub fn lint_only(
|
||||
) -> LinterResult {
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
|
||||
if matches!(target_version, PythonVersion::PY314) && !is_py314_support_enabled(settings) {
|
||||
if matches!(target_version, TargetVersion(Some(PythonVersion::PY314)))
|
||||
&& !is_py314_support_enabled(settings)
|
||||
{
|
||||
warn_user_once!("Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning.");
|
||||
}
|
||||
|
||||
let parsed = source.into_parsed(source_kind, source_type, target_version);
|
||||
let parsed = source.into_parsed(source_kind, source_type, target_version.parser_version());
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::new(source_kind.source_code());
|
||||
@@ -563,14 +565,17 @@ pub fn lint_fix<'a>(
|
||||
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
|
||||
if matches!(target_version, PythonVersion::PY314) && !is_py314_support_enabled(settings) {
|
||||
if matches!(target_version, TargetVersion(Some(PythonVersion::PY314)))
|
||||
&& !is_py314_support_enabled(settings)
|
||||
{
|
||||
warn_user_once!("Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning.");
|
||||
}
|
||||
|
||||
// Continuously fix until the source code stabilizes.
|
||||
loop {
|
||||
// Parse once.
|
||||
let parsed = parse_unchecked_source(&transformed, source_type, target_version);
|
||||
let parsed =
|
||||
parse_unchecked_source(&transformed, source_type, target_version.parser_version());
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::new(transformed.source_code());
|
||||
@@ -972,8 +977,9 @@ mod tests {
|
||||
settings: &LinterSettings,
|
||||
) -> Vec<Message> {
|
||||
let source_type = PySourceType::from(path);
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
let options =
|
||||
ParseOptions::from(source_type).with_target_version(settings.unresolved_target_version);
|
||||
ParseOptions::from(source_type).with_target_version(target_version.parser_version());
|
||||
let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options)
|
||||
.try_into_module()
|
||||
.expect("PySourceType always parses into a module");
|
||||
@@ -998,7 +1004,7 @@ mod tests {
|
||||
source_kind,
|
||||
source_type,
|
||||
&parsed,
|
||||
settings.unresolved_target_version,
|
||||
target_version,
|
||||
);
|
||||
messages.sort_by_key(Ranged::start);
|
||||
messages
|
||||
@@ -1063,6 +1069,53 @@ mod tests {
|
||||
PythonVersion::PY310,
|
||||
"MultipleCaseAssignment"
|
||||
)]
|
||||
#[test_case(
|
||||
"duplicate_match_key",
|
||||
"
|
||||
match x:
|
||||
case {'key': 1, 'key': 2}:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"DuplicateMatchKey"
|
||||
)]
|
||||
#[test_case(
|
||||
"duplicate_match_class_attribute",
|
||||
"
|
||||
match x:
|
||||
case Point(x=1, x=2):
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"DuplicateMatchClassAttribute"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_star_expression",
|
||||
"
|
||||
def func():
|
||||
return *x
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"InvalidStarExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_star_expression_for",
|
||||
"
|
||||
for *x in range(10):
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"InvalidStarExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_star_expression_yield",
|
||||
"
|
||||
def func():
|
||||
yield *x
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"InvalidStarExpression"
|
||||
)]
|
||||
fn test_semantic_errors(
|
||||
name: &str,
|
||||
contents: &str,
|
||||
@@ -1074,7 +1127,7 @@ mod tests {
|
||||
contents,
|
||||
&LinterSettings {
|
||||
rules: settings::rule_table::RuleTable::empty(),
|
||||
unresolved_target_version: python_version,
|
||||
unresolved_target_version: python_version.into(),
|
||||
preview: settings::types::PreviewMode::Enabled,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1092,7 +1145,7 @@ mod tests {
|
||||
&SourceKind::IpyNotebook(Notebook::from_path(path)?),
|
||||
path,
|
||||
&LinterSettings {
|
||||
unresolved_target_version: python_version,
|
||||
unresolved_target_version: python_version.into(),
|
||||
rules: settings::rule_table::RuleTable::empty(),
|
||||
preview: settings::types::PreviewMode::Enabled,
|
||||
..Default::default()
|
||||
@@ -1110,6 +1163,8 @@ mod tests {
|
||||
Rule::LoadBeforeGlobalDeclaration,
|
||||
Path::new("load_before_global_declaration.py")
|
||||
)]
|
||||
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async_function.py"))]
|
||||
#[test_case(Rule::AwaitOutsideAsync, Path::new("async_comprehension.py"))]
|
||||
fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = path.to_string_lossy().to_string();
|
||||
let path = Path::new("resources/test/fixtures/syntax_errors").join(path);
|
||||
@@ -1156,7 +1211,7 @@ mod tests {
|
||||
"pyi019_adds_typing_extensions",
|
||||
PYI019_EXAMPLE,
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
typing_extensions: true,
|
||||
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
|
||||
}
|
||||
@@ -1165,7 +1220,7 @@ mod tests {
|
||||
"pyi019_does_not_add_typing_extensions",
|
||||
PYI019_EXAMPLE,
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
typing_extensions: false,
|
||||
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
|
||||
}
|
||||
@@ -1174,7 +1229,7 @@ mod tests {
|
||||
"pyi019_adds_typing_without_extensions_disabled",
|
||||
PYI019_EXAMPLE,
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY311,
|
||||
unresolved_target_version: PythonVersion::PY311.into(),
|
||||
typing_extensions: true,
|
||||
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
|
||||
}
|
||||
@@ -1183,7 +1238,7 @@ mod tests {
|
||||
"pyi019_adds_typing_with_extensions_disabled",
|
||||
PYI019_EXAMPLE,
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY311,
|
||||
unresolved_target_version: PythonVersion::PY311.into(),
|
||||
typing_extensions: false,
|
||||
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
|
||||
}
|
||||
@@ -1195,7 +1250,7 @@ mod tests {
|
||||
def __new__(cls) -> C: ...
|
||||
",
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion { major: 3, minor: 10 },
|
||||
unresolved_target_version: PythonVersion { major: 3, minor: 10 }.into(),
|
||||
typing_extensions: false,
|
||||
..LinterSettings::for_rule(Rule::NonSelfReturnType)
|
||||
}
|
||||
@@ -1212,7 +1267,7 @@ mod tests {
|
||||
return commons
|
||||
"#,
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion { major: 3, minor: 8 },
|
||||
unresolved_target_version: PythonVersion { major: 3, minor: 8 }.into(),
|
||||
typing_extensions: false,
|
||||
..LinterSettings::for_rule(Rule::FastApiNonAnnotatedDependency)
|
||||
}
|
||||
@@ -1227,7 +1282,7 @@ mod tests {
|
||||
"pyi026_disabled",
|
||||
"Vector = list[float]",
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion { major: 3, minor: 9 },
|
||||
unresolved_target_version: PythonVersion { major: 3, minor: 9 }.into(),
|
||||
typing_extensions: false,
|
||||
..LinterSettings::for_rule(Rule::TypeAliasWithoutAnnotation)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ expression: value
|
||||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
|
||||
@@ -22,6 +22,11 @@ pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/16565
|
||||
pub(crate) const fn is_full_path_match_source_strategy_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// Rule-specific behavior
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/17136
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::rules::numpy::helpers::ImportSearcher;
|
||||
use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher};
|
||||
use ruff_python_ast::name::QualifiedNameBuilder;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{Expr, ExprName, StmtTry};
|
||||
use ruff_python_semantic::Exceptions;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
@@ -47,6 +49,22 @@ pub(crate) fn is_guarded_by_try_except(
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
match expr {
|
||||
Expr::Attribute(_) => {
|
||||
if !semantic.in_exception_handler() {
|
||||
return false;
|
||||
}
|
||||
let Some(try_node) = semantic
|
||||
.current_statements()
|
||||
.find_map(|stmt| stmt.as_try_stmt())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let suspended_exceptions = Exceptions::from_try_stmt(try_node, semantic);
|
||||
if !suspended_exceptions.contains(Exceptions::ATTRIBUTE_ERROR) {
|
||||
return false;
|
||||
}
|
||||
try_block_contains_undeprecated_attribute(try_node, replacement, semantic)
|
||||
}
|
||||
Expr::Name(ExprName { id, .. }) => {
|
||||
let Some(binding_id) = semantic.lookup_symbol(id.as_str()) else {
|
||||
return false;
|
||||
@@ -78,7 +96,31 @@ pub(crate) fn is_guarded_by_try_except(
|
||||
}
|
||||
|
||||
/// Given an [`ast::StmtTry`] node, does the `try` branch of that node
|
||||
/// contain any [`ast::StmtImportFrom`] nodes that indicate the numpy
|
||||
/// contain any [`ast::ExprAttribute`] nodes that indicate the airflow
|
||||
/// member is being accessed from the non-deprecated location?
|
||||
fn try_block_contains_undeprecated_attribute(
|
||||
try_node: &StmtTry,
|
||||
replacement: &Replacement,
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
let Replacement::AutoImport { module, name } = replacement else {
|
||||
return false;
|
||||
};
|
||||
let undeprecated_qualified_name = {
|
||||
let mut builder = QualifiedNameBuilder::default();
|
||||
for part in module.split('.') {
|
||||
builder.push(part);
|
||||
}
|
||||
builder.push(name);
|
||||
builder.build()
|
||||
};
|
||||
let mut attribute_searcher = AttributeSearcher::new(undeprecated_qualified_name, semantic);
|
||||
attribute_searcher.visit_body(&try_node.body);
|
||||
attribute_searcher.found_attribute
|
||||
}
|
||||
|
||||
/// Given an [`ast::StmtTry`] node, does the `try` branch of that node
|
||||
/// contain any [`ast::StmtImportFrom`] nodes that indicate the airflow
|
||||
/// member is being imported from the non-deprecated location?
|
||||
fn try_block_contains_undeprecated_import(try_node: &StmtTry, replacement: &Replacement) -> bool {
|
||||
let Replacement::AutoImport { module, name } = replacement else {
|
||||
|
||||
@@ -710,11 +710,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
// airflow.utils.dag_cycle_tester
|
||||
["dag_cycle_tester", "test_cycle"] => Replacement::None,
|
||||
|
||||
// airflow.utils.dag_parsing_context
|
||||
["dag_parsing_context", "get_parsing_context"] => {
|
||||
Replacement::Name("airflow.sdk.get_parsing_context")
|
||||
}
|
||||
|
||||
// airflow.utils.db
|
||||
["db", "create_session"] => Replacement::None,
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("fastapi").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: ruff_python_ast::PythonVersion::PY38,
|
||||
unresolved_target_version: ruff_python_ast::PythonVersion::PY38.into(),
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -128,7 +128,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_annotations/auto_return_type.py"),
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
unresolved_target_version: PythonVersion::PY38.into(),
|
||||
..LinterSettings::for_rules(vec![
|
||||
Rule::MissingReturnTypeUndocumentedPublicFunction,
|
||||
Rule::MissingReturnTypePrivateFunction,
|
||||
|
||||
@@ -44,7 +44,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_async").join(path),
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -289,11 +289,11 @@ impl Violation for UnixCommandWildcardInjection {
|
||||
}
|
||||
|
||||
/// Check if an expression is a trusted input for subprocess.run.
|
||||
/// We assume that any str or list[str] literal can be trusted.
|
||||
/// We assume that any str, list[str] or tuple[str] literal can be trusted.
|
||||
fn is_trusted_input(arg: &Expr) -> bool {
|
||||
match arg {
|
||||
Expr::StringLiteral(_) => true,
|
||||
Expr::List(ast::ExprList { elts, .. }) => {
|
||||
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
elts.iter().all(|elt| matches!(elt, Expr::StringLiteral(_)))
|
||||
}
|
||||
Expr::Named(named) => is_trusted_input(&named.value),
|
||||
|
||||
@@ -200,3 +200,20 @@ S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
41 | run(e)
|
||||
| ^^^ S603
|
||||
|
|
||||
|
||||
S603.py:46:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
44 | # https://github.com/astral-sh/ruff/issues/17798
|
||||
45 | # Tuple literals are trusted
|
||||
46 | check_output(("literal", "cmd", "using", "tuple"), text=True)
|
||||
| ^^^^^^^^^^^^ S603
|
||||
47 | Popen(("literal", "cmd", "using", "tuple"))
|
||||
|
|
||||
|
||||
S603.py:47:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
45 | # Tuple literals are trusted
|
||||
46 | check_output(("literal", "cmd", "using", "tuple"), text=True)
|
||||
47 | Popen(("literal", "cmd", "using", "tuple"))
|
||||
| ^^^^^ S603
|
||||
|
|
||||
|
||||
@@ -100,7 +100,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_bugbear").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
unresolved_target_version: target_version,
|
||||
unresolved_target_version: target_version.into(),
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -217,7 +217,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_builtins").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
unresolved_target_version: PythonVersion::PY38.into(),
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -30,7 +30,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_future_annotations").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation)
|
||||
},
|
||||
)?;
|
||||
@@ -49,7 +49,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_future_annotations").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -165,7 +165,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_pyi").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
unresolved_target_version: PythonVersion::PY38.into(),
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -92,7 +92,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY39,
|
||||
unresolved_target_version: PythonVersion::PY39.into(),
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -12,10 +12,12 @@ use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::fix;
|
||||
use crate::importer::ImportedMembers;
|
||||
use crate::preview::is_full_path_match_source_strategy_enabled;
|
||||
use crate::rules::flake8_type_checking::helpers::{
|
||||
filter_contained, is_typing_reference, quote_annotation,
|
||||
};
|
||||
use crate::rules::flake8_type_checking::imports::ImportBinding;
|
||||
use crate::rules::isort::categorize::MatchSourceStrategy;
|
||||
use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||
|
||||
/// ## What it does
|
||||
@@ -63,6 +65,12 @@ use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||
/// return len(sized)
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
|
||||
/// the criterion for determining whether an import is first-party
|
||||
/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-third-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-type-checking.quote-annotations`
|
||||
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
|
||||
@@ -138,6 +146,11 @@ impl Violation for TypingOnlyFirstPartyImport {
|
||||
/// return len(df)
|
||||
/// ```
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
|
||||
/// the criterion for determining whether an import is first-party
|
||||
/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-first-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-type-checking.quote-annotations`
|
||||
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
|
||||
@@ -299,9 +312,18 @@ pub(crate) fn typing_only_runtime_import(
|
||||
continue;
|
||||
}
|
||||
|
||||
let source_name = import.source_name().join(".");
|
||||
|
||||
// Categorize the import, using coarse-grained categorization.
|
||||
let match_source_strategy =
|
||||
if is_full_path_match_source_strategy_enabled(checker.settings) {
|
||||
MatchSourceStrategy::FullPath
|
||||
} else {
|
||||
MatchSourceStrategy::Root
|
||||
};
|
||||
|
||||
let import_type = match categorize(
|
||||
&qualified_name.to_string(),
|
||||
&source_name,
|
||||
qualified_name.is_unresolved_import(),
|
||||
&checker.settings.src,
|
||||
checker.package(),
|
||||
@@ -311,6 +333,7 @@ pub(crate) fn typing_only_runtime_import(
|
||||
checker.settings.isort.no_sections,
|
||||
&checker.settings.isort.section_order,
|
||||
&checker.settings.isort.default_section,
|
||||
match_source_strategy,
|
||||
) {
|
||||
ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => {
|
||||
ImportType::FirstParty
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::iter;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fs, iter};
|
||||
|
||||
use log::debug;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
@@ -100,6 +101,7 @@ pub(crate) fn categorize<'a>(
|
||||
no_sections: bool,
|
||||
section_order: &'a [ImportSection],
|
||||
default_section: &'a ImportSection,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
) -> &'a ImportSection {
|
||||
let module_base = module_name.split('.').next().unwrap();
|
||||
let (mut import_type, mut reason) = {
|
||||
@@ -127,7 +129,7 @@ pub(crate) fn categorize<'a>(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::SamePackage,
|
||||
)
|
||||
} else if let Some(src) = match_sources(src, module_base) {
|
||||
} else if let Some(src) = match_sources(src, module_name, match_source_strategy) {
|
||||
(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::SourceMatch(src),
|
||||
@@ -156,20 +158,64 @@ fn same_package(package: Option<PackageRoot<'_>>, module_base: &str) -> bool {
|
||||
.is_some_and(|package| package.ends_with(module_base))
|
||||
}
|
||||
|
||||
fn match_sources<'a>(paths: &'a [PathBuf], base: &str) -> Option<&'a Path> {
|
||||
for path in paths {
|
||||
if let Ok(metadata) = fs::metadata(path.join(base)) {
|
||||
if metadata.is_dir() {
|
||||
return Some(path);
|
||||
/// Returns the source path with respect to which the module `name`
|
||||
/// should be considered first party, or `None` if no path is found.
|
||||
///
|
||||
/// The [`MatchSourceStrategy`] is the criterion used to decide whether
|
||||
/// the module path matches a given source directory.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// - The module named `foo` will match `[SRC]` if `[SRC]/foo` is a directory,
|
||||
/// no matter the strategy.
|
||||
///
|
||||
/// - With `match_source_strategy == MatchSourceStrategy::Root`, the module
|
||||
/// named `foo.baz` will match `[SRC]` if `[SRC]/foo` is a
|
||||
/// directory or `[SRC]/foo.py` exists.
|
||||
///
|
||||
/// - With `match_source_stratgy == MatchSourceStrategy::FullPath`, the module
|
||||
/// named `foo.baz` will match `[SRC]` only if `[SRC]/foo/baz` is a directory,
|
||||
/// or `[SRC]/foo/baz.py` exists or `[SRC]/foo/baz.pyi` exists.
|
||||
fn match_sources<'a>(
|
||||
paths: &'a [PathBuf],
|
||||
name: &str,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
) -> Option<&'a Path> {
|
||||
match match_source_strategy {
|
||||
MatchSourceStrategy::Root => {
|
||||
let base = name.split('.').next()?;
|
||||
for path in paths {
|
||||
if let Ok(metadata) = fs::metadata(path.join(base)) {
|
||||
if metadata.is_dir() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) {
|
||||
if metadata.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) {
|
||||
if metadata.is_file() {
|
||||
return Some(path);
|
||||
MatchSourceStrategy::FullPath => {
|
||||
let relative_path: PathBuf = name.split('.').collect();
|
||||
relative_path.components().next()?;
|
||||
for root in paths {
|
||||
let candidate = root.join(&relative_path);
|
||||
if candidate.is_dir() {
|
||||
return Some(root);
|
||||
}
|
||||
if ["py", "pyi"]
|
||||
.into_iter()
|
||||
.any(|extension| candidate.with_extension(extension).is_file())
|
||||
{
|
||||
return Some(root);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
@@ -183,6 +229,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections: bool,
|
||||
section_order: &'a [ImportSection],
|
||||
default_section: &'a ImportSection,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> {
|
||||
let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default();
|
||||
// Categorize `Stmt::Import`.
|
||||
@@ -198,6 +245,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections,
|
||||
section_order,
|
||||
default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
block_by_type
|
||||
.entry(import_type)
|
||||
@@ -218,6 +266,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections,
|
||||
section_order,
|
||||
default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
block_by_type
|
||||
.entry(classification)
|
||||
@@ -238,6 +287,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections,
|
||||
section_order,
|
||||
default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
block_by_type
|
||||
.entry(classification)
|
||||
@@ -258,6 +308,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections,
|
||||
section_order,
|
||||
default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
block_by_type
|
||||
.entry(classification)
|
||||
@@ -409,3 +460,463 @@ impl fmt::Display for KnownModules {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Rule to determine whether a module path matches
|
||||
/// a relative path from a source directory.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum MatchSourceStrategy {
|
||||
/// Matches if first term in module path is found in file system
|
||||
///
|
||||
/// # Example
|
||||
/// Module is `foo.bar.baz` and `[SRC]/foo` exists
|
||||
Root,
|
||||
/// Matches only if full module path is reflected in file system
|
||||
///
|
||||
/// # Example
|
||||
/// Module is `foo.bar.baz` and `[SRC]/foo/bar/baz` exists
|
||||
FullPath,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::rules::isort::categorize::{match_sources, MatchSourceStrategy};
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::tempdir;
|
||||
|
||||
/// Helper function to create a file with parent directories
|
||||
fn create_file<P: AsRef<Path>>(path: P) {
|
||||
let path = path.as_ref();
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
fs::write(path, "").unwrap();
|
||||
}
|
||||
|
||||
/// Helper function to create a directory and all parent directories
|
||||
fn create_dir<P: AsRef<Path>>(path: P) {
|
||||
fs::create_dir_all(path).unwrap();
|
||||
}
|
||||
|
||||
/// Tests a traditional Python package layout:
|
||||
/// ```
|
||||
/// project/
|
||||
/// └── mypackage/
|
||||
/// ├── __init__.py
|
||||
/// ├── module1.py
|
||||
/// └── module2.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn test_traditional_layout() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let project_dir = temp_dir.path().join("project");
|
||||
|
||||
// Create traditional layout
|
||||
create_dir(project_dir.join("mypackage"));
|
||||
create_file(project_dir.join("mypackage/__init__.py"));
|
||||
create_file(project_dir.join("mypackage/module1.py"));
|
||||
create_file(project_dir.join("mypackage/module2.py"));
|
||||
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
// Test with Root strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "nonexistent", MatchSourceStrategy::Root),
|
||||
None
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Differs in behavior from [`MatchSourceStrategy::Root`]
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.nonexistent",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests a src-based Python package layout:
|
||||
/// ```
|
||||
/// project/
|
||||
/// └── src/
|
||||
/// └── mypackage/
|
||||
/// ├── __init__.py
|
||||
/// └── module1.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn test_src_layout() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let project_dir = temp_dir.path().join("project");
|
||||
let src_dir = project_dir.join("src");
|
||||
|
||||
// Create src layout
|
||||
create_dir(src_dir.join("mypackage"));
|
||||
create_file(src_dir.join("mypackage/__init__.py"));
|
||||
create_file(src_dir.join("mypackage/module1.py"));
|
||||
|
||||
let paths = vec![src_dir.clone()];
|
||||
|
||||
// Test with Root strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
|
||||
Some(src_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root),
|
||||
Some(src_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root),
|
||||
Some(src_dir.as_path())
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
|
||||
Some(src_dir.as_path())
|
||||
);
|
||||
|
||||
// Differs in behavior from [`MatchSourceStrategy::Root`]
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.nonexistent",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests a nested package layout:
|
||||
/// ```
|
||||
/// project/
|
||||
/// └── mypackage/
|
||||
/// ├── __init__.py
|
||||
/// ├── module1.py
|
||||
/// └── subpackage/
|
||||
/// ├── __init__.py
|
||||
/// └── module2.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn test_nested_packages() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let project_dir = temp_dir.path().join("project");
|
||||
|
||||
// Create nested package layout
|
||||
create_dir(project_dir.join("mypackage/subpackage"));
|
||||
create_file(project_dir.join("mypackage/__init__.py"));
|
||||
create_file(project_dir.join("mypackage/module1.py"));
|
||||
create_file(project_dir.join("mypackage/subpackage/__init__.py"));
|
||||
create_file(project_dir.join("mypackage/subpackage/module2.py"));
|
||||
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
// Test with Root strategy
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.subpackage", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.subpackage.module2",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Differs in behavior from [`MatchSourceStrategy::Root`]
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.subpackage.nonexistent",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests a namespace package layout (PEP 420):
|
||||
/// ```
|
||||
/// project/
|
||||
/// └── namespace/ # No __init__.py (namespace package)
|
||||
/// └── package1/
|
||||
/// ├── __init__.py
|
||||
/// └── module1.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn test_namespace_packages() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let project_dir = temp_dir.path().join("project");
|
||||
|
||||
// Create namespace package layout
|
||||
create_dir(project_dir.join("namespace/package1"));
|
||||
create_file(project_dir.join("namespace/package1/__init__.py"));
|
||||
create_file(project_dir.join("namespace/package1/module1.py"));
|
||||
|
||||
let paths = vec![project_dir.clone()];
|
||||
// Test with Root strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "namespace", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "namespace.package1", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"namespace.package2.module1",
|
||||
MatchSourceStrategy::Root
|
||||
),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "namespace.package1", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"namespace.package1.module1",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Differs in behavior from [`MatchSourceStrategy::Root`]
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"namespace.package2.module1",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests a package with type stubs (.pyi files):
|
||||
/// ```
|
||||
/// project/
|
||||
/// └── mypackage/
|
||||
/// ├── __init__.py
|
||||
/// └── module1.pyi # Only .pyi file, no .py
|
||||
/// ```
|
||||
#[test]
|
||||
fn test_type_stubs() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let project_dir = temp_dir.path().join("project");
|
||||
|
||||
// Create package with type stub
|
||||
create_dir(project_dir.join("mypackage"));
|
||||
create_file(project_dir.join("mypackage/__init__.py"));
|
||||
create_file(project_dir.join("mypackage/module1.pyi")); // Only create .pyi file, not .py
|
||||
|
||||
// Test with FullPath strategy
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
// Module "mypackage.module1" should match project_dir using .pyi file
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests a package with both a module and a directory having the same name:
|
||||
/// ```
|
||||
/// project/
|
||||
/// └── mypackage/
|
||||
/// ├── __init__.py
|
||||
/// ├── feature.py # Module with same name as directory
|
||||
/// └── feature/ # Directory with same name as module
|
||||
/// ├── __init__.py
|
||||
/// └── submodule.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn test_same_name_module_and_directory() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let project_dir = temp_dir.path().join("project");
|
||||
|
||||
// Create package with module and directory of the same name
|
||||
create_dir(project_dir.join("mypackage/feature"));
|
||||
create_file(project_dir.join("mypackage/__init__.py"));
|
||||
create_file(project_dir.join("mypackage/feature.py")); // Module with same name as directory
|
||||
create_file(project_dir.join("mypackage/feature/__init__.py"));
|
||||
create_file(project_dir.join("mypackage/feature/submodule.py"));
|
||||
|
||||
// Test with Root strategy
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
// Module "mypackage.feature" should match project_dir (matches the file first)
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.feature", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
// Module "mypackage.feature" should match project_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.feature", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Module "mypackage.feature.submodule" should match project_dir
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.feature.submodule",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests multiple source directories with different packages:
|
||||
/// ```
|
||||
/// project1/
|
||||
/// └── package1/
|
||||
/// ├── __init__.py
|
||||
/// └── module1.py
|
||||
///
|
||||
/// project2/
|
||||
/// └── package2/
|
||||
/// ├── __init__.py
|
||||
/// └── module2.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn test_multiple_source_paths() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let project1_dir = temp_dir.path().join("project1");
|
||||
let project2_dir = temp_dir.path().join("project2");
|
||||
|
||||
// Create files in project1
|
||||
create_dir(project1_dir.join("package1"));
|
||||
create_file(project1_dir.join("package1/__init__.py"));
|
||||
create_file(project1_dir.join("package1/module1.py"));
|
||||
|
||||
// Create files in project2
|
||||
create_dir(project2_dir.join("package2"));
|
||||
create_file(project2_dir.join("package2/__init__.py"));
|
||||
create_file(project2_dir.join("package2/module2.py"));
|
||||
|
||||
// Test with multiple paths in search order
|
||||
let paths = vec![project1_dir.clone(), project2_dir.clone()];
|
||||
|
||||
// Module "package1" should match project1_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths, "package1", MatchSourceStrategy::Root),
|
||||
Some(project1_dir.as_path())
|
||||
);
|
||||
|
||||
// Module "package2" should match project2_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths, "package2", MatchSourceStrategy::Root),
|
||||
Some(project2_dir.as_path())
|
||||
);
|
||||
|
||||
// Try with reversed order to check search order
|
||||
let paths_reversed = vec![project2_dir, project1_dir.clone()];
|
||||
|
||||
// Module "package1" should still match project1_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths_reversed, "package1", MatchSourceStrategy::Root),
|
||||
Some(project1_dir.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests behavior with an empty module name
|
||||
/// ```
|
||||
/// project/
|
||||
/// └── mypackage/
|
||||
/// ```
|
||||
///
|
||||
/// In theory this should never happen since we expect
|
||||
/// module names to have been normalized by the time we
|
||||
/// call `match_sources`. But it is worth noting that the
|
||||
/// behavior is different depending on the [`MatchSourceStrategy`]
|
||||
#[test]
|
||||
fn test_empty_module_name() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let project_dir = temp_dir.path().join("project");
|
||||
|
||||
create_dir(project_dir.join("mypackage"));
|
||||
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
match_sources(&paths, "", MatchSourceStrategy::FullPath),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests behavior with an empty list of source paths
|
||||
#[test]
|
||||
fn test_empty_paths() {
|
||||
let paths: Vec<PathBuf> = vec![];
|
||||
|
||||
// Empty paths should return None
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::path::PathBuf;
|
||||
use annotate::annotate_imports;
|
||||
use block::{Block, Trailer};
|
||||
pub(crate) use categorize::categorize;
|
||||
use categorize::categorize_imports;
|
||||
use categorize::{categorize_imports, MatchSourceStrategy};
|
||||
pub use categorize::{ImportSection, ImportType};
|
||||
use comments::Comment;
|
||||
use normalize::normalize_imports;
|
||||
@@ -76,6 +76,7 @@ pub(crate) fn format_imports(
|
||||
source_type: PySourceType,
|
||||
target_version: PythonVersion,
|
||||
settings: &Settings,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
tokens: &Tokens,
|
||||
) -> String {
|
||||
let trailer = &block.trailer;
|
||||
@@ -103,6 +104,7 @@ pub(crate) fn format_imports(
|
||||
package,
|
||||
target_version,
|
||||
settings,
|
||||
match_source_strategy,
|
||||
);
|
||||
|
||||
if !block_output.is_empty() && !output.is_empty() {
|
||||
@@ -159,6 +161,7 @@ fn format_import_block(
|
||||
package: Option<PackageRoot<'_>>,
|
||||
target_version: PythonVersion,
|
||||
settings: &Settings,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
) -> String {
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum LineInsertion {
|
||||
@@ -169,7 +172,6 @@ fn format_import_block(
|
||||
Inserted,
|
||||
}
|
||||
|
||||
// Categorize by type (e.g., first-party vs. third-party).
|
||||
let mut block_by_type = categorize_imports(
|
||||
block,
|
||||
src,
|
||||
@@ -180,6 +182,7 @@ fn format_import_block(
|
||||
settings.no_sections,
|
||||
&settings.section_order,
|
||||
&settings.default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
@@ -15,6 +15,8 @@ use super::super::block::Block;
|
||||
use super::super::{comments, format_imports};
|
||||
use crate::line_width::LineWidthBuilder;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::is_full_path_match_source_strategy_enabled;
|
||||
use crate::rules::isort::categorize::MatchSourceStrategy;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::Locator;
|
||||
|
||||
@@ -36,6 +38,13 @@ use crate::Locator;
|
||||
/// import numpy as np
|
||||
/// import pandas
|
||||
/// ```
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [`preview`](https://docs.astral.sh/ruff/preview/) mode is enabled, Ruff applies a stricter criterion
|
||||
/// for determining whether an import should be classified as first-party.
|
||||
/// Specifically, for an import of the form `import foo.bar.baz`, Ruff will
|
||||
/// check that `foo/bar`, relative to a [user-specified `src`](https://docs.astral.sh/ruff/settings/#src) directory, contains either
|
||||
/// the directory `baz` or else a file with the name `baz.py` or `baz.pyi`.
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnsortedImports;
|
||||
|
||||
@@ -117,6 +126,12 @@ pub(crate) fn organize_imports(
|
||||
trailing_lines_end(block.imports.last().unwrap(), locator.contents())
|
||||
};
|
||||
|
||||
let match_source_strategy = if is_full_path_match_source_strategy_enabled(settings) {
|
||||
MatchSourceStrategy::FullPath
|
||||
} else {
|
||||
MatchSourceStrategy::Root
|
||||
};
|
||||
|
||||
// Generate the sorted import block.
|
||||
let expected = format_imports(
|
||||
block,
|
||||
@@ -130,6 +145,7 @@ pub(crate) fn organize_imports(
|
||||
source_type,
|
||||
target_version,
|
||||
&settings.isort,
|
||||
match_source_strategy,
|
||||
tokens,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::visitor::{walk_expr, walk_stmt};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast::{statement_visitor, Alias, Stmt, StmtImportFrom};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
/// AST visitor that searches an AST tree for [`ast::StmtImportFrom`] nodes
|
||||
/// that match a certain [`QualifiedName`].
|
||||
@@ -43,3 +48,57 @@ impl StatementVisitor<'_> for ImportSearcher<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AST visitor that searches an AST tree for [`ast::ExprAttribute`] nodes
|
||||
/// that match a certain [`QualifiedName`].
|
||||
pub(crate) struct AttributeSearcher<'a> {
|
||||
attribute_to_find: QualifiedName<'a>,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
pub found_attribute: bool,
|
||||
}
|
||||
|
||||
impl<'a> AttributeSearcher<'a> {
|
||||
pub(crate) fn new(
|
||||
attribute_to_find: QualifiedName<'a>,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
attribute_to_find,
|
||||
semantic,
|
||||
found_attribute: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visitor<'_> for AttributeSearcher<'_> {
|
||||
fn visit_expr(&mut self, expr: &'_ Expr) {
|
||||
if self.found_attribute {
|
||||
return;
|
||||
}
|
||||
if expr.is_attribute_expr()
|
||||
&& self
|
||||
.semantic
|
||||
.resolve_qualified_name(expr)
|
||||
.is_some_and(|qualified_name| qualified_name == self.attribute_to_find)
|
||||
{
|
||||
self.found_attribute = true;
|
||||
return;
|
||||
}
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &ruff_python_ast::Stmt) {
|
||||
if !self.found_attribute {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_body(&mut self, body: &[ruff_python_ast::Stmt]) {
|
||||
for stmt in body {
|
||||
self.visit_stmt(stmt);
|
||||
if self.found_attribute {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::rules::numpy::helpers::ImportSearcher;
|
||||
use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder};
|
||||
use ruff_python_ast::name::QualifiedNameBuilder;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
@@ -823,57 +823,6 @@ fn try_block_contains_undeprecated_attribute(
|
||||
attribute_searcher.found_attribute
|
||||
}
|
||||
|
||||
/// AST visitor that searches an AST tree for [`ast::ExprAttribute`] nodes
|
||||
/// that match a certain [`QualifiedName`].
|
||||
struct AttributeSearcher<'a> {
|
||||
attribute_to_find: QualifiedName<'a>,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
found_attribute: bool,
|
||||
}
|
||||
|
||||
impl<'a> AttributeSearcher<'a> {
|
||||
fn new(attribute_to_find: QualifiedName<'a>, semantic: &'a SemanticModel<'a>) -> Self {
|
||||
Self {
|
||||
attribute_to_find,
|
||||
semantic,
|
||||
found_attribute: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visitor<'_> for AttributeSearcher<'_> {
|
||||
fn visit_expr(&mut self, expr: &'_ Expr) {
|
||||
if self.found_attribute {
|
||||
return;
|
||||
}
|
||||
if expr.is_attribute_expr()
|
||||
&& self
|
||||
.semantic
|
||||
.resolve_qualified_name(expr)
|
||||
.is_some_and(|qualified_name| qualified_name == self.attribute_to_find)
|
||||
{
|
||||
self.found_attribute = true;
|
||||
return;
|
||||
}
|
||||
ast::visitor::walk_expr(self, expr);
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &ruff_python_ast::Stmt) {
|
||||
if !self.found_attribute {
|
||||
ast::visitor::walk_stmt(self, stmt);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_body(&mut self, body: &[ruff_python_ast::Stmt]) {
|
||||
for stmt in body {
|
||||
self.visit_stmt(stmt);
|
||||
if self.found_attribute {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an [`ast::StmtTry`] node, does the `try` branch of that node
|
||||
/// contain any [`ast::StmtImportFrom`] nodes that indicate the numpy
|
||||
/// member is being imported from the non-deprecated location?
|
||||
|
||||
@@ -44,7 +44,7 @@ mod tests {
|
||||
Path::new("perflint").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -219,7 +219,7 @@ mod tests {
|
||||
let diagnostics = test_snippet(
|
||||
"PythonFinalizationError",
|
||||
&LinterSettings {
|
||||
unresolved_target_version: ruff_python_ast::PythonVersion::PY312,
|
||||
unresolved_target_version: ruff_python_ast::PythonVersion::PY312.into(),
|
||||
..LinterSettings::for_rule(Rule::UndefinedName)
|
||||
},
|
||||
);
|
||||
@@ -744,8 +744,9 @@ mod tests {
|
||||
let source_type = PySourceType::default();
|
||||
let source_kind = SourceKind::Python(contents.to_string());
|
||||
let settings = LinterSettings::for_rules(Linter::Pyflakes.rules());
|
||||
let target_version = settings.unresolved_target_version;
|
||||
let options =
|
||||
ParseOptions::from(source_type).with_target_version(settings.unresolved_target_version);
|
||||
ParseOptions::from(source_type).with_target_version(target_version.parser_version());
|
||||
let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options)
|
||||
.try_into_module()
|
||||
.expect("PySourceType always parses into a module");
|
||||
@@ -770,7 +771,7 @@ mod tests {
|
||||
&source_kind,
|
||||
source_type,
|
||||
&parsed,
|
||||
settings.unresolved_target_version,
|
||||
target_version,
|
||||
);
|
||||
messages.sort_by_key(Ranged::start);
|
||||
let actual = messages
|
||||
|
||||
@@ -16,8 +16,11 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix;
|
||||
use crate::preview::is_dunder_init_fix_unused_import_enabled;
|
||||
use crate::preview::{
|
||||
is_dunder_init_fix_unused_import_enabled, is_full_path_match_source_strategy_enabled,
|
||||
};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::isort::categorize::MatchSourceStrategy;
|
||||
use crate::rules::{isort, isort::ImportSection, isort::ImportType};
|
||||
|
||||
/// ## What it does
|
||||
@@ -88,6 +91,11 @@ use crate::rules::{isort, isort::ImportSection, isort::ImportType};
|
||||
/// print("numpy is not installed")
|
||||
/// ```
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
|
||||
/// the criterion for determining whether an import is first-party
|
||||
/// is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.ignore-init-module-imports`
|
||||
/// - `lint.pyflakes.allowed-unused-imports`
|
||||
@@ -222,10 +230,15 @@ enum UnusedImportContext {
|
||||
}
|
||||
|
||||
fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
|
||||
let qualified_name = import.qualified_name();
|
||||
let source_name = import.source_name().join(".");
|
||||
let match_source_strategy = if is_full_path_match_source_strategy_enabled(checker.settings) {
|
||||
MatchSourceStrategy::FullPath
|
||||
} else {
|
||||
MatchSourceStrategy::Root
|
||||
};
|
||||
let category = isort::categorize(
|
||||
&qualified_name.to_string(),
|
||||
qualified_name.is_unresolved_import(),
|
||||
&source_name,
|
||||
import.qualified_name().is_unresolved_import(),
|
||||
&checker.settings.src,
|
||||
checker.package(),
|
||||
checker.settings.isort.detect_same_package,
|
||||
@@ -234,6 +247,7 @@ fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
|
||||
checker.settings.isort.no_sections,
|
||||
&checker.settings.isort.section_order,
|
||||
&checker.settings.isort.default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
matches! {
|
||||
category,
|
||||
|
||||
@@ -19,6 +19,20 @@ use ruff_text_size::Ranged;
|
||||
/// Prefer `sys.exit()`, as the `sys` module is guaranteed to exist in all
|
||||
/// contexts.
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This fix is always unsafe. When replacing `exit` or `quit` with `sys.exit`,
|
||||
/// the behavior can change in the following ways:
|
||||
///
|
||||
/// 1. If the code runs in an environment where the `site` module is not imported
|
||||
/// (e.g., with `python -S`), the original code would raise a `NameError`, while
|
||||
/// the fixed code would execute normally.
|
||||
///
|
||||
/// 2. `site.exit` and `sys.exit` handle tuple arguments differently. `site.exit`
|
||||
/// treats tuples as regular objects and always returns exit code 1, while `sys.exit`
|
||||
/// interprets tuple contents to determine the exit code: an empty tuple () results in
|
||||
/// exit code 0, and a single-element tuple like (2,) uses that element's value (2) as
|
||||
/// the exit code.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// if __name__ == "__main__":
|
||||
|
||||
@@ -15,6 +15,34 @@ use ruff_python_ast::PythonVersion;
|
||||
/// Dunder names are not meant to be called explicitly and, in most cases, can
|
||||
/// be replaced with builtins or operators.
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This fix is always unsafe. When replacing dunder method calls with operators
|
||||
/// or builtins, the behavior can change in the following ways:
|
||||
///
|
||||
/// 1. Types may implement only a subset of related dunder methods. Calling a
|
||||
/// missing dunder method directly returns `NotImplemented`, but using the
|
||||
/// equivalent operator raises a `TypeError`.
|
||||
/// ```python
|
||||
/// class C: pass
|
||||
/// c = C()
|
||||
/// c.__gt__(1) # before fix: NotImplemented
|
||||
/// c > 1 # after fix: raises TypeError
|
||||
/// ```
|
||||
/// 2. Instance-assigned dunder methods are ignored by operators and builtins.
|
||||
/// ```python
|
||||
/// class C: pass
|
||||
/// c = C()
|
||||
/// c.__bool__ = lambda: False
|
||||
/// c.__bool__() # before fix: False
|
||||
/// bool(c) # after fix: True
|
||||
/// ```
|
||||
///
|
||||
/// 3. Even with built-in types, behavior can differ.
|
||||
/// ```python
|
||||
/// (1).__gt__(1.0) # before fix: NotImplemented
|
||||
/// 1 > 1.0 # after fix: False
|
||||
/// ```
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// three = (3.0).__str__()
|
||||
|
||||
@@ -139,7 +139,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/UP041.py"),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias)
|
||||
},
|
||||
)?;
|
||||
@@ -152,7 +152,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/UP040.py"),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY311,
|
||||
unresolved_target_version: PythonVersion::PY311.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias)
|
||||
},
|
||||
)?;
|
||||
@@ -168,7 +168,7 @@ mod tests {
|
||||
pyupgrade: pyupgrade::settings::Settings {
|
||||
keep_runtime_typing: true,
|
||||
},
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
|
||||
},
|
||||
)?;
|
||||
@@ -184,7 +184,7 @@ mod tests {
|
||||
pyupgrade: pyupgrade::settings::Settings {
|
||||
keep_runtime_typing: true,
|
||||
},
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
|
||||
},
|
||||
)?;
|
||||
@@ -197,7 +197,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/future_annotations.py"),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
|
||||
},
|
||||
)?;
|
||||
@@ -210,7 +210,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/future_annotations.py"),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
|
||||
},
|
||||
)?;
|
||||
@@ -223,7 +223,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/future_annotations.py"),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37.into(),
|
||||
..settings::LinterSettings::for_rules([
|
||||
Rule::NonPEP604AnnotationUnion,
|
||||
Rule::NonPEP604AnnotationOptional,
|
||||
@@ -239,7 +239,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/future_annotations.py"),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
..settings::LinterSettings::for_rules([
|
||||
Rule::NonPEP604AnnotationUnion,
|
||||
Rule::NonPEP604AnnotationOptional,
|
||||
@@ -255,7 +255,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/UP017.py"),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY311,
|
||||
unresolved_target_version: PythonVersion::PY311.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC)
|
||||
},
|
||||
)?;
|
||||
@@ -268,7 +268,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/UP044.py"),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY311,
|
||||
unresolved_target_version: PythonVersion::PY311.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -161,13 +161,14 @@ pub(crate) fn native_literals(
|
||||
keywords,
|
||||
range: _,
|
||||
},
|
||||
range: _,
|
||||
range: call_range,
|
||||
} = call;
|
||||
|
||||
if !keywords.is_empty() || args.len() > 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let tokens = checker.tokens();
|
||||
let semantic = checker.semantic();
|
||||
|
||||
let Some(builtin) = semantic.resolve_builtin_symbol(func) else {
|
||||
@@ -244,7 +245,20 @@ pub(crate) fn native_literals(
|
||||
|
||||
let arg_code = checker.locator().slice(arg);
|
||||
|
||||
let content = match (parent_expr, literal_type, has_unary_op) {
|
||||
let mut needs_space = false;
|
||||
// Look for the `Rpar` token of the call expression and check if there is a keyword token right
|
||||
// next to it without any space separating them. Without this check, the fix for this
|
||||
// rule would create a syntax error.
|
||||
// Ex) `bool(True)and None` no space between `)` and the keyword `and`.
|
||||
//
|
||||
// Subtract 1 from the end of the range to include `Rpar` token in the slice.
|
||||
if let [paren_token, next_token, ..] = tokens.after(call_range.sub_end(1.into()).end())
|
||||
{
|
||||
needs_space = next_token.kind().is_keyword()
|
||||
&& paren_token.range().end() == next_token.range().start();
|
||||
}
|
||||
|
||||
let mut content = match (parent_expr, literal_type, has_unary_op) {
|
||||
// Expressions including newlines must be parenthesised to be valid syntax
|
||||
(_, _, true) if find_newline(arg_code).is_some() => format!("({arg_code})"),
|
||||
|
||||
@@ -265,6 +279,10 @@ pub(crate) fn native_literals(
|
||||
_ => arg_code.to_string(),
|
||||
};
|
||||
|
||||
if needs_space {
|
||||
content.push(' ');
|
||||
}
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(call.range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -602,6 +602,8 @@ UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
|
||||
85 | | ipsum''' # Comment
|
||||
86 | | ).foo
|
||||
| |_^ UP018
|
||||
87 |
|
||||
88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
|
|
||||
= help: Replace with string literal
|
||||
|
||||
@@ -615,3 +617,80 @@ UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
|
||||
86 |-).foo
|
||||
83 |+'''Lorem
|
||||
84 |+ ipsum'''.foo
|
||||
87 85 |
|
||||
88 86 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 87 | bool(True)and None
|
||||
|
||||
UP018.py:89:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal)
|
||||
|
|
||||
88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 | bool(True)and None
|
||||
| ^^^^^^^^^^ UP018
|
||||
90 | int(1)and None
|
||||
91 | float(1.)and None
|
||||
|
|
||||
= help: Replace with boolean literal
|
||||
|
||||
ℹ Safe fix
|
||||
86 86 | ).foo
|
||||
87 87 |
|
||||
88 88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 |-bool(True)and None
|
||||
89 |+True and None
|
||||
90 90 | int(1)and None
|
||||
91 91 | float(1.)and None
|
||||
92 92 | bool(True)and()
|
||||
|
||||
UP018.py:90:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 | bool(True)and None
|
||||
90 | int(1)and None
|
||||
| ^^^^^^ UP018
|
||||
91 | float(1.)and None
|
||||
92 | bool(True)and()
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
87 87 |
|
||||
88 88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 89 | bool(True)and None
|
||||
90 |-int(1)and None
|
||||
90 |+1 and None
|
||||
91 91 | float(1.)and None
|
||||
92 92 | bool(True)and()
|
||||
|
||||
UP018.py:91:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
|
|
||||
89 | bool(True)and None
|
||||
90 | int(1)and None
|
||||
91 | float(1.)and None
|
||||
| ^^^^^^^^^ UP018
|
||||
92 | bool(True)and()
|
||||
|
|
||||
= help: Replace with float literal
|
||||
|
||||
ℹ Safe fix
|
||||
88 88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 89 | bool(True)and None
|
||||
90 90 | int(1)and None
|
||||
91 |-float(1.)and None
|
||||
91 |+1. and None
|
||||
92 92 | bool(True)and()
|
||||
|
||||
UP018.py:92:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal)
|
||||
|
|
||||
90 | int(1)and None
|
||||
91 | float(1.)and None
|
||||
92 | bool(True)and()
|
||||
| ^^^^^^^^^^ UP018
|
||||
|
|
||||
= help: Replace with boolean literal
|
||||
|
||||
ℹ Safe fix
|
||||
89 89 | bool(True)and None
|
||||
90 90 | int(1)and None
|
||||
91 91 | float(1.)and None
|
||||
92 |-bool(True)and()
|
||||
92 |+True and()
|
||||
|
||||
@@ -99,6 +99,7 @@ mod tests {
|
||||
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_1.py"))]
|
||||
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_2.py"))]
|
||||
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_3.py"))]
|
||||
#[test_case(Rule::InEmptyCollection, Path::new("RUF060.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
|
||||
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]
|
||||
@@ -135,7 +136,7 @@ mod tests {
|
||||
ruff: super::settings::Settings {
|
||||
parenthesize_tuple_in_subscript: false,
|
||||
},
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -72,6 +72,11 @@ use super::super::typing::type_hint_explicitly_allows_none;
|
||||
/// ## Options
|
||||
/// - `target-version`
|
||||
///
|
||||
/// ## Fix safety
|
||||
///
|
||||
/// This fix is always marked as unsafe because it can change the behavior of code that relies on
|
||||
/// type hints, and it assumes the default value is always appropriate—which might not be the case.
|
||||
///
|
||||
/// [PEP 484]: https://peps.python.org/pep-0484/#union-types
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct ImplicitOptional {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{self as ast, CmpOp, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for membership tests on empty collections (such as `list`, `tuple`, `set` or `dict`).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// If the collection is always empty, the check is unnecessary, and can be removed.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// if 1 not in set():
|
||||
/// print("got it!")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// print("got it!")
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct InEmptyCollection;
|
||||
|
||||
impl Violation for InEmptyCollection {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Unnecessary membership test on empty collection".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF060
|
||||
pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare) {
|
||||
let [op] = &*compare.ops else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !matches!(op, CmpOp::In | CmpOp::NotIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
let [right] = &*compare.comparators else {
|
||||
return;
|
||||
};
|
||||
|
||||
let semantic = checker.semantic();
|
||||
let collection_methods = [
|
||||
"list",
|
||||
"tuple",
|
||||
"set",
|
||||
"frozenset",
|
||||
"dict",
|
||||
"bytes",
|
||||
"bytearray",
|
||||
"str",
|
||||
];
|
||||
|
||||
let is_empty_collection = match right {
|
||||
Expr::List(ast::ExprList { elts, .. }) => elts.is_empty(),
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.is_empty(),
|
||||
Expr::Set(ast::ExprSet { elts, .. }) => elts.is_empty(),
|
||||
Expr::Dict(ast::ExprDict { items, .. }) => items.is_empty(),
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(),
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
|
||||
Expr::FString(s) => s
|
||||
.value
|
||||
.elements()
|
||||
.all(|elt| elt.as_literal().is_some_and(|elt| elt.is_empty())),
|
||||
Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
arguments,
|
||||
range: _,
|
||||
}) => {
|
||||
arguments.is_empty()
|
||||
&& collection_methods
|
||||
.iter()
|
||||
.any(|s| semantic.match_builtin_expr(func, s))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_empty_collection {
|
||||
checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range()));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ pub(crate) use function_call_in_dataclass_default::*;
|
||||
pub(crate) use if_key_in_dict_del::*;
|
||||
pub(crate) use implicit_classvar_in_dataclass::*;
|
||||
pub(crate) use implicit_optional::*;
|
||||
pub(crate) use in_empty_collection::*;
|
||||
pub(crate) use incorrectly_parenthesized_tuple_in_subscript::*;
|
||||
pub(crate) use indented_form_feed::*;
|
||||
pub(crate) use invalid_assert_message_literal_argument::*;
|
||||
@@ -73,6 +74,7 @@ mod helpers;
|
||||
mod if_key_in_dict_del;
|
||||
mod implicit_classvar_in_dataclass;
|
||||
mod implicit_optional;
|
||||
mod in_empty_collection;
|
||||
mod incorrectly_parenthesized_tuple_in_subscript;
|
||||
mod indented_form_feed;
|
||||
mod invalid_assert_message_literal_argument;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF060.py:2:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
1 | # Errors
|
||||
2 | 1 in []
|
||||
| ^^^^^^^ RUF060
|
||||
3 | 1 not in []
|
||||
4 | 2 in list()
|
||||
|
|
||||
|
||||
RUF060.py:3:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
1 | # Errors
|
||||
2 | 1 in []
|
||||
3 | 1 not in []
|
||||
| ^^^^^^^^^^^ RUF060
|
||||
4 | 2 in list()
|
||||
5 | 2 not in list()
|
||||
|
|
||||
|
||||
RUF060.py:4:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
2 | 1 in []
|
||||
3 | 1 not in []
|
||||
4 | 2 in list()
|
||||
| ^^^^^^^^^^^ RUF060
|
||||
5 | 2 not in list()
|
||||
6 | _ in ()
|
||||
|
|
||||
|
||||
RUF060.py:5:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
3 | 1 not in []
|
||||
4 | 2 in list()
|
||||
5 | 2 not in list()
|
||||
| ^^^^^^^^^^^^^^^ RUF060
|
||||
6 | _ in ()
|
||||
7 | _ not in ()
|
||||
|
|
||||
|
||||
RUF060.py:6:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
4 | 2 in list()
|
||||
5 | 2 not in list()
|
||||
6 | _ in ()
|
||||
| ^^^^^^^ RUF060
|
||||
7 | _ not in ()
|
||||
8 | 'x' in tuple()
|
||||
|
|
||||
|
||||
RUF060.py:7:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
5 | 2 not in list()
|
||||
6 | _ in ()
|
||||
7 | _ not in ()
|
||||
| ^^^^^^^^^^^ RUF060
|
||||
8 | 'x' in tuple()
|
||||
9 | 'y' not in tuple()
|
||||
|
|
||||
|
||||
RUF060.py:8:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
6 | _ in ()
|
||||
7 | _ not in ()
|
||||
8 | 'x' in tuple()
|
||||
| ^^^^^^^^^^^^^^ RUF060
|
||||
9 | 'y' not in tuple()
|
||||
10 | 'a' in set()
|
||||
|
|
||||
|
||||
RUF060.py:9:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
7 | _ not in ()
|
||||
8 | 'x' in tuple()
|
||||
9 | 'y' not in tuple()
|
||||
| ^^^^^^^^^^^^^^^^^^ RUF060
|
||||
10 | 'a' in set()
|
||||
11 | 'a' not in set()
|
||||
|
|
||||
|
||||
RUF060.py:10:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
8 | 'x' in tuple()
|
||||
9 | 'y' not in tuple()
|
||||
10 | 'a' in set()
|
||||
| ^^^^^^^^^^^^ RUF060
|
||||
11 | 'a' not in set()
|
||||
12 | 'b' in {}
|
||||
|
|
||||
|
||||
RUF060.py:11:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
9 | 'y' not in tuple()
|
||||
10 | 'a' in set()
|
||||
11 | 'a' not in set()
|
||||
| ^^^^^^^^^^^^^^^^ RUF060
|
||||
12 | 'b' in {}
|
||||
13 | 'b' not in {}
|
||||
|
|
||||
|
||||
RUF060.py:12:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
10 | 'a' in set()
|
||||
11 | 'a' not in set()
|
||||
12 | 'b' in {}
|
||||
| ^^^^^^^^^ RUF060
|
||||
13 | 'b' not in {}
|
||||
14 | 1 in dict()
|
||||
|
|
||||
|
||||
RUF060.py:13:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
11 | 'a' not in set()
|
||||
12 | 'b' in {}
|
||||
13 | 'b' not in {}
|
||||
| ^^^^^^^^^^^^^ RUF060
|
||||
14 | 1 in dict()
|
||||
15 | 2 not in dict()
|
||||
|
|
||||
|
||||
RUF060.py:14:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
12 | 'b' in {}
|
||||
13 | 'b' not in {}
|
||||
14 | 1 in dict()
|
||||
| ^^^^^^^^^^^ RUF060
|
||||
15 | 2 not in dict()
|
||||
16 | "a" in ""
|
||||
|
|
||||
|
||||
RUF060.py:15:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
13 | 'b' not in {}
|
||||
14 | 1 in dict()
|
||||
15 | 2 not in dict()
|
||||
| ^^^^^^^^^^^^^^^ RUF060
|
||||
16 | "a" in ""
|
||||
17 | b'c' in b""
|
||||
|
|
||||
|
||||
RUF060.py:16:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
14 | 1 in dict()
|
||||
15 | 2 not in dict()
|
||||
16 | "a" in ""
|
||||
| ^^^^^^^^^ RUF060
|
||||
17 | b'c' in b""
|
||||
18 | "b" in f""
|
||||
|
|
||||
|
||||
RUF060.py:17:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
15 | 2 not in dict()
|
||||
16 | "a" in ""
|
||||
17 | b'c' in b""
|
||||
| ^^^^^^^^^^^ RUF060
|
||||
18 | "b" in f""
|
||||
19 | b"a" in bytearray()
|
||||
|
|
||||
|
||||
RUF060.py:18:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
16 | "a" in ""
|
||||
17 | b'c' in b""
|
||||
18 | "b" in f""
|
||||
| ^^^^^^^^^^ RUF060
|
||||
19 | b"a" in bytearray()
|
||||
20 | b"a" in bytes()
|
||||
|
|
||||
|
||||
RUF060.py:19:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
17 | b'c' in b""
|
||||
18 | "b" in f""
|
||||
19 | b"a" in bytearray()
|
||||
| ^^^^^^^^^^^^^^^^^^^ RUF060
|
||||
20 | b"a" in bytes()
|
||||
21 | 1 in frozenset()
|
||||
|
|
||||
|
||||
RUF060.py:20:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
18 | "b" in f""
|
||||
19 | b"a" in bytearray()
|
||||
20 | b"a" in bytes()
|
||||
| ^^^^^^^^^^^^^^^ RUF060
|
||||
21 | 1 in frozenset()
|
||||
|
|
||||
|
||||
RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection
|
||||
|
|
||||
19 | b"a" in bytearray()
|
||||
20 | b"a" in bytes()
|
||||
21 | 1 in frozenset()
|
||||
| ^^^^^^^^^^^^^^^^ RUF060
|
||||
22 |
|
||||
23 | # OK
|
||||
|
|
||||
@@ -225,7 +225,7 @@ pub struct LinterSettings {
|
||||
///
|
||||
/// Otherwise, see [`LinterSettings::resolve_target_version`] for a way to obtain the Python
|
||||
/// version for a given file, while respecting the overrides in `per_file_target_version`.
|
||||
pub unresolved_target_version: PythonVersion,
|
||||
pub unresolved_target_version: TargetVersion,
|
||||
/// Path-specific overrides to `unresolved_target_version`.
|
||||
///
|
||||
/// If you have a `Checker` available, see its `target_version` method instead.
|
||||
@@ -378,7 +378,7 @@ impl LinterSettings {
|
||||
pub fn for_rule(rule_code: Rule) -> Self {
|
||||
Self {
|
||||
rules: RuleTable::from_iter([rule_code]),
|
||||
unresolved_target_version: PythonVersion::latest(),
|
||||
unresolved_target_version: PythonVersion::latest().into(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
@@ -386,7 +386,7 @@ impl LinterSettings {
|
||||
pub fn for_rules(rules: impl IntoIterator<Item = Rule>) -> Self {
|
||||
Self {
|
||||
rules: RuleTable::from_iter(rules),
|
||||
unresolved_target_version: PythonVersion::latest(),
|
||||
unresolved_target_version: PythonVersion::latest().into(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
@@ -394,7 +394,7 @@ impl LinterSettings {
|
||||
pub fn new(project_root: &Path) -> Self {
|
||||
Self {
|
||||
exclude: FilePatternSet::default(),
|
||||
unresolved_target_version: PythonVersion::default(),
|
||||
unresolved_target_version: TargetVersion(None),
|
||||
per_file_target_version: CompiledPerFileTargetVersionList::default(),
|
||||
project_root: project_root.to_path_buf(),
|
||||
rules: DEFAULT_SELECTORS
|
||||
@@ -458,19 +458,19 @@ impl LinterSettings {
|
||||
|
||||
#[must_use]
|
||||
pub fn with_target_version(mut self, target_version: PythonVersion) -> Self {
|
||||
self.unresolved_target_version = target_version;
|
||||
self.unresolved_target_version = target_version.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Resolve the [`PythonVersion`] to use for linting.
|
||||
/// Resolve the [`TargetVersion`] to use for linting.
|
||||
///
|
||||
/// This method respects the per-file version overrides in
|
||||
/// [`LinterSettings::per_file_target_version`] and falls back on
|
||||
/// [`LinterSettings::unresolved_target_version`] if none of the override patterns match.
|
||||
pub fn resolve_target_version(&self, path: &Path) -> PythonVersion {
|
||||
pub fn resolve_target_version(&self, path: &Path) -> TargetVersion {
|
||||
self.per_file_target_version
|
||||
.is_match(path)
|
||||
.unwrap_or(self.unresolved_target_version)
|
||||
.map_or(self.unresolved_target_version, TargetVersion::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,3 +479,48 @@ impl Default for LinterSettings {
|
||||
Self::new(fs::get_cwd())
|
||||
}
|
||||
}
|
||||
|
||||
/// A thin wrapper around `Option<PythonVersion>` to clarify the reason for different `unwrap`
|
||||
/// calls in various places.
|
||||
///
|
||||
/// For example, we want to default to `PythonVersion::latest()` for parsing and detecting semantic
|
||||
/// syntax errors because this will minimize version-related diagnostics when the Python version is
|
||||
/// unset. In contrast, we want to default to `PythonVersion::default()` for lint rules. These
|
||||
/// correspond to the [`TargetVersion::parser_version`] and [`TargetVersion::linter_version`]
|
||||
/// methods, respectively.
|
||||
#[derive(Debug, Clone, Copy, CacheKey)]
|
||||
pub struct TargetVersion(pub Option<PythonVersion>);
|
||||
|
||||
impl TargetVersion {
|
||||
/// Return the [`PythonVersion`] to use for parsing.
|
||||
///
|
||||
/// This will be either the Python version specified by the user or the latest supported
|
||||
/// version if unset.
|
||||
pub fn parser_version(&self) -> PythonVersion {
|
||||
self.0.unwrap_or_else(PythonVersion::latest)
|
||||
}
|
||||
|
||||
/// Return the [`PythonVersion`] to use for version-dependent lint rules.
|
||||
///
|
||||
/// This will either be the Python version specified by the user or the default Python version
|
||||
/// if unset.
|
||||
pub fn linter_version(&self) -> PythonVersion {
|
||||
self.0.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PythonVersion> for TargetVersion {
|
||||
fn from(value: PythonVersion) -> Self {
|
||||
Self(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// manual inlining of display_settings!
|
||||
match self.0 {
|
||||
Some(value) => write!(f, "{value}"),
|
||||
None => f.write_str("none"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
resources/test/fixtures/syntax_errors/async_comprehension.py:5:8: PLE1142 `await` should be used within an async function
|
||||
|
|
||||
4 | def regular_function():
|
||||
5 | [x async for x in elements(1)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1142
|
||||
6 |
|
||||
7 | async with elements(1) as x:
|
||||
|
|
||||
|
||||
resources/test/fixtures/syntax_errors/async_comprehension.py:7:5: PLE1142 `await` should be used within an async function
|
||||
|
|
||||
5 | [x async for x in elements(1)]
|
||||
6 |
|
||||
7 | / async with elements(1) as x:
|
||||
8 | | pass
|
||||
| |____________^ PLE1142
|
||||
9 |
|
||||
10 | async for _ in elements(1):
|
||||
|
|
||||
|
||||
resources/test/fixtures/syntax_errors/async_comprehension.py:10:5: PLE1142 `await` should be used within an async function
|
||||
|
|
||||
8 | pass
|
||||
9 |
|
||||
10 | / async for _ in elements(1):
|
||||
11 | | pass
|
||||
| |____________^ PLE1142
|
||||
|
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
resources/test/fixtures/syntax_errors/await_outside_async_function.py:2:5: PLE1142 `await` should be used within an async function
|
||||
|
|
||||
1 | def func():
|
||||
2 | await 1
|
||||
| ^^^^^^^ PLE1142
|
||||
3 |
|
||||
4 | # Top-level await
|
||||
|
|
||||
|
||||
resources/test/fixtures/syntax_errors/await_outside_async_function.py:5:1: PLE1142 `await` should be used within an async function
|
||||
|
|
||||
4 | # Top-level await
|
||||
5 | await 1
|
||||
| ^^^^^^^ PLE1142
|
||||
|
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
<filename>:3:21: SyntaxError: attribute name `x` repeated in class pattern
|
||||
|
|
||||
2 | match x:
|
||||
3 | case Point(x=1, x=2):
|
||||
| ^
|
||||
4 | pass
|
||||
|
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
<filename>:3:21: SyntaxError: mapping pattern checks duplicate key `'key'`
|
||||
|
|
||||
2 | match x:
|
||||
3 | case {'key': 1, 'key': 2}:
|
||||
| ^^^^^
|
||||
4 | pass
|
||||
|
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
<filename>:3:12: SyntaxError: Starred expression cannot be used here
|
||||
|
|
||||
2 | def func():
|
||||
3 | return *x
|
||||
| ^^
|
||||
|
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
<filename>:2:5: SyntaxError: Starred expression cannot be used here
|
||||
|
|
||||
2 | for *x in range(10):
|
||||
| ^^
|
||||
3 | pass
|
||||
|
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
<filename>:3:11: SyntaxError: Starred expression cannot be used here
|
||||
|
|
||||
2 | def func():
|
||||
3 | yield *x
|
||||
| ^^
|
||||
|
|
||||
@@ -110,7 +110,8 @@ pub(crate) fn test_contents<'a>(
|
||||
) -> (Vec<Message>, Cow<'a, SourceKind>) {
|
||||
let source_type = PySourceType::from(path);
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
let options = ParseOptions::from(source_type).with_target_version(target_version);
|
||||
let options =
|
||||
ParseOptions::from(source_type).with_target_version(target_version.parser_version());
|
||||
let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options.clone())
|
||||
.try_into_module()
|
||||
.expect("PySourceType always parses into a module");
|
||||
|
||||
@@ -73,6 +73,10 @@ impl PythonVersion {
|
||||
pub fn supports_pep_701(self) -> bool {
|
||||
self >= Self::PY312
|
||||
}
|
||||
|
||||
pub fn defers_annotations(self) -> bool {
|
||||
self >= Self::PY314
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PythonVersion {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
sum(x for x in range(10), 5)
|
||||
total(1, 2, x for x in range(5), 6)
|
||||
sum(x for x in range(10),)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
zip((x for x in range(10)), (y for y in range(10)))
|
||||
sum(x for x in range(10))
|
||||
sum((x for x in range(10)),)
|
||||
|
||||
@@ -661,117 +661,120 @@ impl<'src> Parser<'src> {
|
||||
let mut seen_keyword_argument = false; // foo = 1
|
||||
let mut seen_keyword_unpacking = false; // **foo
|
||||
|
||||
self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| {
|
||||
let argument_start = parser.node_start();
|
||||
if parser.eat(TokenKind::DoubleStar) {
|
||||
let value = parser.parse_conditional_expression_or_higher();
|
||||
|
||||
keywords.push(ast::Keyword {
|
||||
arg: None,
|
||||
value: value.expr,
|
||||
range: parser.node_range(argument_start),
|
||||
});
|
||||
|
||||
seen_keyword_unpacking = true;
|
||||
} else {
|
||||
let start = parser.node_start();
|
||||
let mut parsed_expr = parser
|
||||
.parse_named_expression_or_higher(ExpressionContext::starred_conditional());
|
||||
|
||||
match parser.current_token_kind() {
|
||||
TokenKind::Async | TokenKind::For => {
|
||||
if parsed_expr.is_unparenthesized_starred_expr() {
|
||||
parser.add_error(
|
||||
ParseErrorType::IterableUnpackingInComprehension,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
|
||||
parsed_expr = Expr::Generator(parser.parse_generator_expression(
|
||||
parsed_expr.expr,
|
||||
start,
|
||||
Parenthesized::No,
|
||||
))
|
||||
.into();
|
||||
}
|
||||
_ => {
|
||||
if seen_keyword_unpacking && parsed_expr.is_unparenthesized_starred_expr() {
|
||||
parser.add_error(
|
||||
ParseErrorType::InvalidArgumentUnpackingOrder,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arg_range = parser.node_range(start);
|
||||
if parser.eat(TokenKind::Equal) {
|
||||
seen_keyword_argument = true;
|
||||
let arg = if let ParsedExpr {
|
||||
expr: Expr::Name(ident_expr),
|
||||
is_parenthesized,
|
||||
} = parsed_expr
|
||||
{
|
||||
// test_ok parenthesized_kwarg_py37
|
||||
// # parse_options: {"target-version": "3.7"}
|
||||
// f((a)=1)
|
||||
|
||||
// test_err parenthesized_kwarg_py38
|
||||
// # parse_options: {"target-version": "3.8"}
|
||||
// f((a)=1)
|
||||
// f((a) = 1)
|
||||
// f( ( a ) = 1)
|
||||
|
||||
if is_parenthesized {
|
||||
parser.add_unsupported_syntax_error(
|
||||
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName,
|
||||
arg_range,
|
||||
);
|
||||
}
|
||||
|
||||
ast::Identifier {
|
||||
id: ident_expr.id,
|
||||
range: ident_expr.range,
|
||||
}
|
||||
} else {
|
||||
// TODO(dhruvmanila): Parser shouldn't drop the `parsed_expr` if it's
|
||||
// not a name expression. We could add the expression into `args` but
|
||||
// that means the error is a missing comma instead.
|
||||
parser.add_error(
|
||||
ParseErrorType::OtherError("Expected a parameter name".to_string()),
|
||||
&parsed_expr,
|
||||
);
|
||||
ast::Identifier {
|
||||
id: Name::empty(),
|
||||
range: parsed_expr.range(),
|
||||
}
|
||||
};
|
||||
|
||||
let has_trailing_comma =
|
||||
self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| {
|
||||
let argument_start = parser.node_start();
|
||||
if parser.eat(TokenKind::DoubleStar) {
|
||||
let value = parser.parse_conditional_expression_or_higher();
|
||||
|
||||
keywords.push(ast::Keyword {
|
||||
arg: Some(arg),
|
||||
arg: None,
|
||||
value: value.expr,
|
||||
range: parser.node_range(argument_start),
|
||||
});
|
||||
|
||||
seen_keyword_unpacking = true;
|
||||
} else {
|
||||
if !parsed_expr.is_unparenthesized_starred_expr() {
|
||||
if seen_keyword_unpacking {
|
||||
parser.add_error(
|
||||
ParseErrorType::PositionalAfterKeywordUnpacking,
|
||||
&parsed_expr,
|
||||
);
|
||||
} else if seen_keyword_argument {
|
||||
parser.add_error(
|
||||
ParseErrorType::PositionalAfterKeywordArgument,
|
||||
&parsed_expr,
|
||||
);
|
||||
let start = parser.node_start();
|
||||
let mut parsed_expr = parser
|
||||
.parse_named_expression_or_higher(ExpressionContext::starred_conditional());
|
||||
|
||||
match parser.current_token_kind() {
|
||||
TokenKind::Async | TokenKind::For => {
|
||||
if parsed_expr.is_unparenthesized_starred_expr() {
|
||||
parser.add_error(
|
||||
ParseErrorType::IterableUnpackingInComprehension,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
|
||||
parsed_expr = Expr::Generator(parser.parse_generator_expression(
|
||||
parsed_expr.expr,
|
||||
start,
|
||||
Parenthesized::No,
|
||||
))
|
||||
.into();
|
||||
}
|
||||
_ => {
|
||||
if seen_keyword_unpacking
|
||||
&& parsed_expr.is_unparenthesized_starred_expr()
|
||||
{
|
||||
parser.add_error(
|
||||
ParseErrorType::InvalidArgumentUnpackingOrder,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
args.push(parsed_expr.expr);
|
||||
|
||||
let arg_range = parser.node_range(start);
|
||||
if parser.eat(TokenKind::Equal) {
|
||||
seen_keyword_argument = true;
|
||||
let arg = if let ParsedExpr {
|
||||
expr: Expr::Name(ident_expr),
|
||||
is_parenthesized,
|
||||
} = parsed_expr
|
||||
{
|
||||
// test_ok parenthesized_kwarg_py37
|
||||
// # parse_options: {"target-version": "3.7"}
|
||||
// f((a)=1)
|
||||
|
||||
// test_err parenthesized_kwarg_py38
|
||||
// # parse_options: {"target-version": "3.8"}
|
||||
// f((a)=1)
|
||||
// f((a) = 1)
|
||||
// f( ( a ) = 1)
|
||||
|
||||
if is_parenthesized {
|
||||
parser.add_unsupported_syntax_error(
|
||||
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName,
|
||||
arg_range,
|
||||
);
|
||||
}
|
||||
|
||||
ast::Identifier {
|
||||
id: ident_expr.id,
|
||||
range: ident_expr.range,
|
||||
}
|
||||
} else {
|
||||
// TODO(dhruvmanila): Parser shouldn't drop the `parsed_expr` if it's
|
||||
// not a name expression. We could add the expression into `args` but
|
||||
// that means the error is a missing comma instead.
|
||||
parser.add_error(
|
||||
ParseErrorType::OtherError("Expected a parameter name".to_string()),
|
||||
&parsed_expr,
|
||||
);
|
||||
ast::Identifier {
|
||||
id: Name::empty(),
|
||||
range: parsed_expr.range(),
|
||||
}
|
||||
};
|
||||
|
||||
let value = parser.parse_conditional_expression_or_higher();
|
||||
|
||||
keywords.push(ast::Keyword {
|
||||
arg: Some(arg),
|
||||
value: value.expr,
|
||||
range: parser.node_range(argument_start),
|
||||
});
|
||||
} else {
|
||||
if !parsed_expr.is_unparenthesized_starred_expr() {
|
||||
if seen_keyword_unpacking {
|
||||
parser.add_error(
|
||||
ParseErrorType::PositionalAfterKeywordUnpacking,
|
||||
&parsed_expr,
|
||||
);
|
||||
} else if seen_keyword_argument {
|
||||
parser.add_error(
|
||||
ParseErrorType::PositionalAfterKeywordArgument,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
}
|
||||
args.push(parsed_expr.expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.expect(TokenKind::Rpar);
|
||||
|
||||
@@ -781,7 +784,7 @@ impl<'src> Parser<'src> {
|
||||
keywords: keywords.into_boxed_slice(),
|
||||
};
|
||||
|
||||
self.validate_arguments(&arguments);
|
||||
self.validate_arguments(&arguments, has_trailing_comma);
|
||||
|
||||
arguments
|
||||
}
|
||||
@@ -2521,9 +2524,9 @@ impl<'src> Parser<'src> {
|
||||
|
||||
/// Performs the following validations on the function call arguments:
|
||||
/// 1. There aren't any duplicate keyword argument
|
||||
/// 2. If there are more than one argument (positional or keyword), all generator expressions
|
||||
/// present should be parenthesized.
|
||||
fn validate_arguments(&mut self, arguments: &ast::Arguments) {
|
||||
/// 2. If there are more than one argument (positional or keyword) or a single argument with a
|
||||
/// trailing comma, all generator expressions present should be parenthesized.
|
||||
fn validate_arguments(&mut self, arguments: &ast::Arguments, has_trailing_comma: bool) {
|
||||
let mut all_arg_names =
|
||||
FxHashSet::with_capacity_and_hasher(arguments.keywords.len(), FxBuildHasher);
|
||||
|
||||
@@ -2541,7 +2544,7 @@ impl<'src> Parser<'src> {
|
||||
}
|
||||
}
|
||||
|
||||
if arguments.len() > 1 {
|
||||
if has_trailing_comma || arguments.len() > 1 {
|
||||
for arg in &*arguments.args {
|
||||
if let Some(ast::ExprGenerator {
|
||||
range,
|
||||
@@ -2550,11 +2553,14 @@ impl<'src> Parser<'src> {
|
||||
}) = arg.as_generator_expr()
|
||||
{
|
||||
// test_ok args_unparenthesized_generator
|
||||
// zip((x for x in range(10)), (y for y in range(10)))
|
||||
// sum(x for x in range(10))
|
||||
// sum((x for x in range(10)),)
|
||||
|
||||
// test_err args_unparenthesized_generator
|
||||
// sum(x for x in range(10), 5)
|
||||
// total(1, 2, x for x in range(5), 6)
|
||||
// sum(x for x in range(10),)
|
||||
self.add_error(ParseErrorType::UnparenthesizedGeneratorExpression, range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,17 +539,19 @@ impl<'src> Parser<'src> {
|
||||
}
|
||||
|
||||
/// Parses a comma separated list of elements where each element is parsed
|
||||
/// sing the given `parse_element` function.
|
||||
/// using the given `parse_element` function.
|
||||
///
|
||||
/// The difference between this function and `parse_comma_separated_list_into_vec`
|
||||
/// is that this function does not return the parsed elements. Instead, it is the
|
||||
/// caller's responsibility to handle the parsed elements. This is the reason
|
||||
/// that the `parse_element` parameter is bound to [`FnMut`] instead of [`Fn`].
|
||||
///
|
||||
/// Returns `true` if there is a trailing comma present.
|
||||
fn parse_comma_separated_list(
|
||||
&mut self,
|
||||
recovery_context_kind: RecoveryContextKind,
|
||||
mut parse_element: impl FnMut(&mut Parser<'src>),
|
||||
) {
|
||||
) -> bool {
|
||||
let mut progress = ParserProgress::default();
|
||||
|
||||
let saved_context = self.recovery_context;
|
||||
@@ -659,6 +661,8 @@ impl<'src> Parser<'src> {
|
||||
}
|
||||
|
||||
self.recovery_context = saved_context;
|
||||
|
||||
trailing_comma_range.is_some()
|
||||
}
|
||||
|
||||
#[cold]
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/err/args_unparenthesized_generator.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
range: 0..65,
|
||||
range: 0..92,
|
||||
body: [
|
||||
Expr(
|
||||
StmtExpr {
|
||||
@@ -194,6 +193,82 @@ Module(
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 65..91,
|
||||
value: Call(
|
||||
ExprCall {
|
||||
range: 65..91,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 65..68,
|
||||
id: Name("sum"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 68..91,
|
||||
args: [
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 69..89,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 69..70,
|
||||
id: Name("x"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 71..89,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 75..76,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 80..89,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 80..85,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 85..89,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 86..88,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: false,
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -204,6 +279,7 @@ Module(
|
||||
1 | sum(x for x in range(10), 5)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here
|
||||
2 | total(1, 2, x for x in range(5), 6)
|
||||
3 | sum(x for x in range(10),)
|
||||
|
|
||||
|
||||
|
||||
@@ -211,4 +287,13 @@ Module(
|
||||
1 | sum(x for x in range(10), 5)
|
||||
2 | total(1, 2, x for x in range(5), 6)
|
||||
| ^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here
|
||||
3 | sum(x for x in range(10),)
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | sum(x for x in range(10), 5)
|
||||
2 | total(1, 2, x for x in range(5), 6)
|
||||
3 | sum(x for x in range(10),)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here
|
||||
|
|
||||
|
||||
@@ -1,67 +1,195 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/ok/args_unparenthesized_generator.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
range: 0..26,
|
||||
range: 0..107,
|
||||
body: [
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..25,
|
||||
range: 0..51,
|
||||
value: Call(
|
||||
ExprCall {
|
||||
range: 0..25,
|
||||
range: 0..51,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 0..3,
|
||||
id: Name("sum"),
|
||||
id: Name("zip"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 3..25,
|
||||
range: 3..51,
|
||||
args: [
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 4..24,
|
||||
range: 4..26,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 4..5,
|
||||
range: 5..6,
|
||||
id: Name("x"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 6..24,
|
||||
range: 7..25,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 10..11,
|
||||
range: 11..12,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 15..24,
|
||||
range: 16..25,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 15..20,
|
||||
range: 16..21,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 20..24,
|
||||
range: 21..25,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 21..23,
|
||||
range: 22..24,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 28..50,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 29..30,
|
||||
id: Name("y"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 31..49,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 35..36,
|
||||
id: Name("y"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 40..49,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 40..45,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 45..49,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 46..48,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 52..77,
|
||||
value: Call(
|
||||
ExprCall {
|
||||
range: 52..77,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 52..55,
|
||||
id: Name("sum"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 55..77,
|
||||
args: [
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 56..76,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 56..57,
|
||||
id: Name("x"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 58..76,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 62..63,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 67..76,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 67..72,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 72..76,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 73..75,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
@@ -86,6 +214,82 @@ Module(
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 78..106,
|
||||
value: Call(
|
||||
ExprCall {
|
||||
range: 78..106,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 78..81,
|
||||
id: Name("sum"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 81..106,
|
||||
args: [
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 82..104,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 83..84,
|
||||
id: Name("x"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 85..103,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 89..90,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 94..103,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 94..99,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 99..103,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 100..102,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -714,6 +714,15 @@ pub trait Imported<'a> {
|
||||
/// Returns the member name of the imported symbol. For a straight import, this is equivalent
|
||||
/// to the qualified name; for a `from` import, this is the name of the imported symbol.
|
||||
fn member_name(&self) -> Cow<'a, str>;
|
||||
|
||||
/// Returns the source module of the imported symbol.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// - `import foo` returns `["foo"]`
|
||||
/// - `import foo.bar` returns `["foo","bar"]`
|
||||
/// - `from foo import bar` returns `["foo"]`
|
||||
fn source_name(&self) -> &[&'a str];
|
||||
}
|
||||
|
||||
impl<'a> Imported<'a> for Import<'a> {
|
||||
@@ -731,6 +740,10 @@ impl<'a> Imported<'a> for Import<'a> {
|
||||
fn member_name(&self) -> Cow<'a, str> {
|
||||
Cow::Owned(self.qualified_name().to_string())
|
||||
}
|
||||
|
||||
fn source_name(&self) -> &[&'a str] {
|
||||
self.qualified_name.segments()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Imported<'a> for SubmoduleImport<'a> {
|
||||
@@ -748,6 +761,10 @@ impl<'a> Imported<'a> for SubmoduleImport<'a> {
|
||||
fn member_name(&self) -> Cow<'a, str> {
|
||||
Cow::Owned(self.qualified_name().to_string())
|
||||
}
|
||||
|
||||
fn source_name(&self) -> &[&'a str] {
|
||||
self.qualified_name.segments()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Imported<'a> for FromImport<'a> {
|
||||
@@ -765,6 +782,10 @@ impl<'a> Imported<'a> for FromImport<'a> {
|
||||
fn member_name(&self) -> Cow<'a, str> {
|
||||
Cow::Borrowed(self.qualified_name.segments()[self.qualified_name.segments().len() - 1])
|
||||
}
|
||||
|
||||
fn source_name(&self) -> &[&'a str] {
|
||||
self.module_name()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an import [`BindingKind`] that can be any of the three types of imports.
|
||||
@@ -799,6 +820,14 @@ impl<'ast> Imported<'ast> for AnyImport<'_, 'ast> {
|
||||
Self::FromImport(import) => import.member_name(),
|
||||
}
|
||||
}
|
||||
|
||||
fn source_name(&self) -> &[&'ast str] {
|
||||
match self {
|
||||
Self::Import(import) => import.source_name(),
|
||||
Self::SubmoduleImport(import) => import.source_name(),
|
||||
Self::FromImport(import) => import.source_name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -101,7 +101,8 @@ pub(crate) fn check(
|
||||
settings.linter.unresolved_target_version
|
||||
};
|
||||
|
||||
let parse_options = ParseOptions::from(source_type).with_target_version(target_version);
|
||||
let parse_options =
|
||||
ParseOptions::from(source_type).with_target_version(target_version.parser_version());
|
||||
|
||||
// Parse once.
|
||||
let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), parse_options)
|
||||
|
||||
@@ -166,7 +166,8 @@ impl Workspace {
|
||||
let target_version = self.settings.linter.unresolved_target_version;
|
||||
|
||||
// Parse once.
|
||||
let options = ParseOptions::from(source_type).with_target_version(target_version);
|
||||
let options =
|
||||
ParseOptions::from(source_type).with_target_version(target_version.parser_version());
|
||||
let parsed = parse_unchecked(source_kind.source_code(), options)
|
||||
.try_into_module()
|
||||
.expect("`PySourceType` always parses to a `ModModule`.");
|
||||
|
||||
@@ -65,7 +65,7 @@ fn syntax_error() {
|
||||
fn unsupported_syntax_error() {
|
||||
check!(
|
||||
"match 2:\n case 1: ...",
|
||||
r#"{"preview": true}"#,
|
||||
r#"{"preview": true, "target-version": "py39"}"#,
|
||||
[ExpandedMessage {
|
||||
code: None,
|
||||
message: "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)".to_string(),
|
||||
|
||||
@@ -33,7 +33,9 @@ use ruff_linter::settings::types::{
|
||||
FilePatternSet, GlobPath, OutputFormat, PerFileIgnore, PerFileTargetVersion, PreviewMode,
|
||||
RequiredVersion, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS};
|
||||
use ruff_linter::settings::{
|
||||
LinterSettings, TargetVersion, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS,
|
||||
};
|
||||
use ruff_linter::{
|
||||
fs, warn_user_once, warn_user_once_by_id, warn_user_once_by_message, RuleSelector,
|
||||
RUFF_PKG_VERSION,
|
||||
@@ -164,6 +166,7 @@ impl Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
let linter_target_version = TargetVersion(self.target_version);
|
||||
let target_version = self.target_version.unwrap_or_default();
|
||||
let global_preview = self.preview.unwrap_or_default();
|
||||
|
||||
@@ -278,7 +281,7 @@ impl Configuration {
|
||||
exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?,
|
||||
extension: self.extension.unwrap_or_default(),
|
||||
preview: lint_preview,
|
||||
unresolved_target_version: target_version,
|
||||
unresolved_target_version: linter_target_version,
|
||||
per_file_target_version,
|
||||
project_root: project_root.to_path_buf(),
|
||||
allowed_confusables: lint
|
||||
|
||||
@@ -5,7 +5,8 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
# Releases occur in this other repository!
|
||||
repository = "https://github.com/astral-sh/ty/"
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -20,7 +21,8 @@ ty_server = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
argfile = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help"] }
|
||||
clap = { workspace = true, features = ["wrap_help", "string"] }
|
||||
clap_complete_command = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
crossbeam = { workspace = true }
|
||||
|
||||
@@ -8,15 +8,43 @@ fn main() {
|
||||
// The workspace root directory is not available without walking up the tree
|
||||
// https://github.com/rust-lang/cargo/issues/3946
|
||||
let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("..");
|
||||
|
||||
version_info(&workspace_root);
|
||||
commit_info(&workspace_root);
|
||||
|
||||
let target = std::env::var("TARGET").unwrap();
|
||||
println!("cargo::rustc-env=RUST_HOST_TARGET={target}");
|
||||
}
|
||||
|
||||
/// Retrieve the version from the `dist-workspace.toml` file and set `TY_VERSION`.
|
||||
fn version_info(workspace_root: &Path) {
|
||||
let dist_file = workspace_root.join("dist-workspace.toml");
|
||||
if !dist_file.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed={}", dist_file.display());
|
||||
|
||||
let dist_file = fs::read_to_string(dist_file);
|
||||
if let Ok(dist_file) = dist_file {
|
||||
let lines = dist_file.lines();
|
||||
for line in lines {
|
||||
if line.starts_with("version =") {
|
||||
let (_key, version) = line.split_once('=').unwrap();
|
||||
println!(
|
||||
"cargo::rustc-env=TY_VERSION={}",
|
||||
version.trim().trim_matches('"')
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve commit information from the Git repository.
|
||||
fn commit_info(workspace_root: &Path) {
|
||||
// If not in a git repository, do not attempt to retrieve commit information
|
||||
let git_dir = workspace_root.join(".git");
|
||||
@@ -48,7 +76,8 @@ fn commit_info(workspace_root: &Path) {
|
||||
.arg("-1")
|
||||
.arg("--date=short")
|
||||
.arg("--abbrev=9")
|
||||
.arg("--format=%H %h %cd %(describe)")
|
||||
.arg("--format=%H %h %cd %(describe:tags)")
|
||||
.current_dir(workspace_root)
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => output,
|
||||
@@ -65,7 +94,9 @@ fn commit_info(workspace_root: &Path) {
|
||||
// https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem
|
||||
if let Some(describe) = parts.next() {
|
||||
let mut describe_parts = describe.split('-');
|
||||
let _last_tag = describe_parts.next().unwrap();
|
||||
let last_tag = describe_parts.next().unwrap();
|
||||
|
||||
println!("cargo::rustc-env=TY_LAST_TAG={last_tag}");
|
||||
|
||||
// If this is the tagged commit, this component will be missing
|
||||
println!(
|
||||
|
||||
@@ -31,7 +31,7 @@ The `TY_MAX_PARALLELISM` environment variable, meanwhile, can be used to control
|
||||
By default, ty will attempt to parallelize its work so that multiple files are checked simultaneously,
|
||||
but this can result in a confused logging output where messages from different threads are intertwined and non
|
||||
determinism.
|
||||
To switch off parallelism entirely and have more readable logs, use `TY_MAX_PARALLELSIM=1` (or `RAYON_NUM_THREADS=1`).
|
||||
To switch off parallelism entirely and have more readable logs, use `TY_MAX_PARALLELISM=1` (or `RAYON_NUM_THREADS=1`).
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use ty_python_semantic::lint;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, name = "ty", about = "An extremely fast Python type checker.")]
|
||||
#[command(version)]
|
||||
#[command(long_version = crate::version::version())]
|
||||
pub(crate) struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Command,
|
||||
@@ -24,6 +24,10 @@ pub(crate) enum Command {
|
||||
|
||||
/// Display ty's version
|
||||
Version,
|
||||
|
||||
/// Generate shell completion
|
||||
#[clap(hide = true)]
|
||||
GenerateShellCompletion { shell: clap_complete_command::Shell },
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::sync::Mutex;
|
||||
use crate::args::{Args, CheckCommand, Command, TerminalColor};
|
||||
use crate::logging::setup_tracing;
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
@@ -68,6 +68,10 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
Command::Server => run_server().map(|()| ExitStatus::Success),
|
||||
Command::Check(check_args) => run_check(check_args),
|
||||
Command::Version => version().map(|()| ExitStatus::Success),
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Args::command(), &mut stdout());
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +310,10 @@ impl MainLoop {
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
|
||||
if max_severity.is_fatal() {
|
||||
tracing::warn!("A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.");
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(match max_severity {
|
||||
Severity::Info => ExitStatus::Success,
|
||||
@@ -401,6 +409,12 @@ fn set_colored_override(color: Option<TerminalColor>) {
|
||||
fn setup_rayon() {
|
||||
ThreadPoolBuilder::default()
|
||||
.num_threads(max_parallelism().get())
|
||||
// Use a reasonably large stack size to avoid running into stack overflows too easily. The
|
||||
// size was chosen in such a way as to still be able to handle large expressions involving
|
||||
// binary operators (x + x + … + x) both during the AST walk in semantic index building as
|
||||
// well as during type checking. Using this stack size, we can handle handle expressions
|
||||
// that are several times larger than the corresponding limits in existing type checkers.
|
||||
.stack_size(16 * 1024 * 1024)
|
||||
.build_global()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ pub(crate) struct CommitInfo {
|
||||
short_commit_hash: String,
|
||||
commit_date: String,
|
||||
commits_since_last_tag: u32,
|
||||
last_tag: Option<String>,
|
||||
}
|
||||
|
||||
/// ty's version.
|
||||
@@ -34,6 +35,12 @@ impl fmt::Display for VersionInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VersionInfo> for clap::builder::Str {
|
||||
fn from(val: VersionInfo) -> Self {
|
||||
val.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns information about ty's version.
|
||||
pub(crate) fn version() -> VersionInfo {
|
||||
// Environment variables are only read at compile-time
|
||||
@@ -43,9 +50,6 @@ pub(crate) fn version() -> VersionInfo {
|
||||
};
|
||||
}
|
||||
|
||||
// This version is pulled from Cargo.toml and set by Cargo
|
||||
let version = option_env_str!("CARGO_PKG_VERSION").unwrap();
|
||||
|
||||
// Commit info is pulled from git and set by `build.rs`
|
||||
let commit_info = option_env_str!("TY_COMMIT_SHORT_HASH").map(|short_commit_hash| CommitInfo {
|
||||
short_commit_hash,
|
||||
@@ -53,6 +57,22 @@ pub(crate) fn version() -> VersionInfo {
|
||||
commits_since_last_tag: option_env_str!("TY_LAST_TAG_DISTANCE")
|
||||
.as_deref()
|
||||
.map_or(0, |value| value.parse::<u32>().unwrap_or(0)),
|
||||
last_tag: option_env_str!("TY_LAST_TAG"),
|
||||
});
|
||||
|
||||
// The version is pulled from `dist-workspace.toml` and set by `build.rs`
|
||||
let version = option_env_str!("TY_VERSION").unwrap_or_else(|| {
|
||||
// If missing, using the last tag
|
||||
commit_info
|
||||
.as_ref()
|
||||
.and_then(|info| {
|
||||
info.last_tag.as_ref().map(|tag| {
|
||||
tag.strip_prefix("v")
|
||||
.map(std::string::ToString::to_string)
|
||||
.unwrap_or(tag.clone())
|
||||
})
|
||||
})
|
||||
.unwrap_or("unknown".to_string())
|
||||
});
|
||||
|
||||
VersionInfo {
|
||||
@@ -84,6 +104,7 @@ mod tests {
|
||||
short_commit_hash: "53b0f5d92".to_string(),
|
||||
commit_date: "2023-10-19".to_string(),
|
||||
commits_since_last_tag: 0,
|
||||
last_tag: None,
|
||||
}),
|
||||
};
|
||||
assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)");
|
||||
@@ -97,6 +118,7 @@ mod tests {
|
||||
short_commit_hash: "53b0f5d92".to_string(),
|
||||
commit_date: "2023-10-19".to_string(),
|
||||
commits_since_last_tag: 24,
|
||||
last_tag: None,
|
||||
}),
|
||||
};
|
||||
assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use anyhow::Context;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
@@ -149,6 +150,7 @@ fn config_override_python_version() -> anyhow::Result<()> {
|
||||
5 | print(sys.last_exc)
|
||||
| ^^^^^^^^^^^^
|
||||
|
|
||||
info: `lint:unresolved-attribute` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -275,7 +277,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-import: Cannot resolve import `utils`
|
||||
error: lint:unresolved-import: Cannot resolve imported module `utils`
|
||||
--> test.py:2:6
|
||||
|
|
||||
2 | from utils import add
|
||||
@@ -283,6 +285,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | stat = add(10, 15)
|
||||
|
|
||||
info: `lint:unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -383,6 +386,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: `lint:division-by-zero` is enabled by default
|
||||
|
||||
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> test.py:7:7
|
||||
@@ -392,6 +396,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
7 | print(x) # possibly-unresolved-reference
|
||||
| ^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -419,6 +424,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: `lint:division-by-zero` was selected in the configuration file
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -451,7 +457,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-import: Cannot resolve import `does_not_exit`
|
||||
error: lint:unresolved-import: Cannot resolve imported module `does_not_exit`
|
||||
--> test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
@@ -459,6 +465,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
info: `lint:unresolved-import` is enabled by default
|
||||
|
||||
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
|
||||
--> test.py:4:5
|
||||
@@ -470,6 +477,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
5 |
|
||||
6 | for a in range(0, int(y)):
|
||||
|
|
||||
info: `lint:division-by-zero` is enabled by default
|
||||
|
||||
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> test.py:9:7
|
||||
@@ -479,6 +487,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
9 | print(x) # possibly-unresolved-reference
|
||||
| ^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
|
||||
Found 3 diagnostics
|
||||
|
||||
@@ -498,7 +507,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-import: Cannot resolve import `does_not_exit`
|
||||
warning: lint:unresolved-import: Cannot resolve imported module `does_not_exit`
|
||||
--> test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
@@ -506,6 +515,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
info: `lint:unresolved-import` was selected on the command line
|
||||
|
||||
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
|
||||
--> test.py:4:5
|
||||
@@ -517,6 +527,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
5 |
|
||||
6 | for a in range(0, int(y)):
|
||||
|
|
||||
info: `lint:division-by-zero` was selected on the command line
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -557,6 +568,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: `lint:division-by-zero` is enabled by default
|
||||
|
||||
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> test.py:7:7
|
||||
@@ -566,6 +578,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
7 | print(x) # possibly-unresolved-reference
|
||||
| ^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -594,6 +607,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: `lint:division-by-zero` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -671,6 +685,7 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -754,6 +769,7 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -786,6 +802,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -816,6 +833,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
@@ -824,6 +842,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: `lint:non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -854,6 +873,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
@@ -862,6 +882,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: `lint:non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -892,6 +913,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
@@ -900,6 +922,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: `lint:non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -953,6 +976,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: `lint:division-by-zero` was selected in the configuration file
|
||||
|
||||
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> main.py:7:7
|
||||
@@ -962,6 +986,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
7 | print(x)
|
||||
| ^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -995,6 +1020,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: `lint:division-by-zero` was selected in the configuration file
|
||||
|
||||
error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> main.py:7:7
|
||||
@@ -1004,6 +1030,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
7 | print(x)
|
||||
| ^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` was selected in the configuration file
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -1051,8 +1078,9 @@ fn check_specific_paths() -> anyhow::Result<()> {
|
||||
2 | y = 4 / 0 # error: division-by-zero
|
||||
| ^^^^^
|
||||
|
|
||||
info: `lint:division-by-zero` is enabled by default
|
||||
|
||||
error: lint:unresolved-import: Cannot resolve import `main2`
|
||||
error: lint:unresolved-import: Cannot resolve imported module `main2`
|
||||
--> project/other.py:2:6
|
||||
|
|
||||
2 | from main2 import z # error: unresolved-import
|
||||
@@ -1060,13 +1088,15 @@ fn check_specific_paths() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | print(z)
|
||||
|
|
||||
info: `lint:unresolved-import` is enabled by default
|
||||
|
||||
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
|
||||
error: lint:unresolved-import: Cannot resolve imported module `does_not_exist`
|
||||
--> project/tests/test_main.py:2:8
|
||||
|
|
||||
2 | import does_not_exist # error: unresolved-import
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `lint:unresolved-import` is enabled by default
|
||||
|
||||
Found 3 diagnostics
|
||||
|
||||
@@ -1082,7 +1112,7 @@ fn check_specific_paths() -> anyhow::Result<()> {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-import: Cannot resolve import `main2`
|
||||
error: lint:unresolved-import: Cannot resolve imported module `main2`
|
||||
--> project/other.py:2:6
|
||||
|
|
||||
2 | from main2 import z # error: unresolved-import
|
||||
@@ -1090,13 +1120,15 @@ fn check_specific_paths() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | print(z)
|
||||
|
|
||||
info: `lint:unresolved-import` is enabled by default
|
||||
|
||||
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
|
||||
error: lint:unresolved-import: Cannot resolve imported module `does_not_exist`
|
||||
--> project/tests/test_main.py:2:8
|
||||
|
|
||||
2 | import does_not_exist # error: unresolved-import
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `lint:unresolved-import` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -1195,6 +1227,42 @@ fn concise_revealed_type() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
|
||||
let mut content = String::new();
|
||||
writeln!(
|
||||
&mut content,
|
||||
"
|
||||
from typing_extensions import reveal_type
|
||||
total = 1{plus_one_repeated}
|
||||
reveal_type(total)
|
||||
",
|
||||
plus_one_repeated = " + 1".repeat(2000 - 1)
|
||||
)?;
|
||||
|
||||
let case = TestCase::with_file("test.py", &ruff_python_trivia::textwrap::dedent(&content))?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:4:1
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1...
|
||||
4 | reveal_type(total)
|
||||
| ^^^^^^^^^^^^^^^^^^ `Literal[2000]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
_temp_dir: TempDir,
|
||||
_settings_scope: SettingsBindDropGuard,
|
||||
|
||||
@@ -6,6 +6,7 @@ use ruff_text_size::TextSize;
|
||||
|
||||
use crate::Db;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Completion {
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub trait Db: SemanticDb + Upcast<dyn SemanticDb> + Upcast<dyn SourceDb> {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::Db;
|
||||
use ruff_db::files::{File, Files};
|
||||
@@ -16,6 +16,8 @@ pub(crate) mod tests {
|
||||
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use ty_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
@@ -23,31 +25,35 @@ pub(crate) mod tests {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Events,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let events = Events::default();
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: ty_vendored::file_system().clone(),
|
||||
events: Arc::default(),
|
||||
events,
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
@@ -127,12 +133,5 @@ pub(crate) mod tests {
|
||||
impl Db for TestDb {}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/17792
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class C: ...
|
||||
|
||||
|
||||
def f(arg: C):
|
||||
pass
|
||||
|
||||
|
||||
x, _ = f(1)
|
||||
|
||||
assert x
|
||||
@@ -37,7 +37,19 @@ impl ProjectDatabase {
|
||||
{
|
||||
let mut db = Self {
|
||||
project: None,
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(if tracing::enabled!(tracing::Level::TRACE) {
|
||||
Some(Box::new({
|
||||
move |event: Event| {
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::trace!("Salsa event: {event:?}");
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
files: Files::default(),
|
||||
system: Arc::new(system),
|
||||
};
|
||||
@@ -156,20 +168,7 @@ impl SourceDb for ProjectDatabase {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for ProjectDatabase {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> Event) {
|
||||
if !tracing::enabled!(tracing::Level::TRACE) {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = event();
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::trace!("Salsa event: {event:?}");
|
||||
}
|
||||
}
|
||||
impl salsa::Database for ProjectDatabase {}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for ProjectDatabase {
|
||||
@@ -206,9 +205,7 @@ mod format {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use salsa::Event;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
@@ -221,11 +218,13 @@ pub(crate) mod tests {
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
use crate::{Project, ProjectMetadata};
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
events: Arc<std::sync::Mutex<Vec<Event>>>,
|
||||
events: Events,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
@@ -234,12 +233,19 @@ pub(crate) mod tests {
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new(project: ProjectMetadata) -> Self {
|
||||
let events = Events::default();
|
||||
let mut db = Self {
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: ty_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
events: Arc::default(),
|
||||
events,
|
||||
project: None,
|
||||
};
|
||||
|
||||
@@ -251,13 +257,9 @@ pub(crate) mod tests {
|
||||
|
||||
impl TestDb {
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
}
|
||||
@@ -332,10 +334,5 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> Event) {
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event());
|
||||
}
|
||||
}
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
|
||||
@@ -219,34 +219,10 @@ impl Project {
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
.unwrap();
|
||||
// We sort diagnostics in a way that keeps them in source order
|
||||
// and grouped by file. After that, we fall back to severity
|
||||
// (with fatal messages sorting before info messages) and then
|
||||
// finally the diagnostic ID.
|
||||
file_diagnostics.sort_by(|d1, d2| {
|
||||
if let (Some(span1), Some(span2)) = (d1.primary_span(), d2.primary_span()) {
|
||||
let order = span1
|
||||
.file()
|
||||
.path(db)
|
||||
.as_str()
|
||||
.cmp(span2.file().path(db).as_str());
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
|
||||
if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) {
|
||||
let order = range1.start().cmp(&range2.start());
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reverse so that, e.g., Fatal sorts before Info.
|
||||
let order = d1.severity().cmp(&d2.severity()).reverse();
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
d1.id().cmp(&d2.id())
|
||||
file_diagnostics.sort_by(|left, right| {
|
||||
left.rendering_sort_key(db)
|
||||
.cmp(&right.rendering_sort_key(db))
|
||||
});
|
||||
diagnostics.extend(file_diagnostics);
|
||||
diagnostics
|
||||
|
||||
@@ -76,7 +76,7 @@ from typing_extensions import Annotated
|
||||
class C(Annotated[int, "foo"]): ...
|
||||
|
||||
# TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]`
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], @Todo(Inference of subscript on special form), Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, @Todo(Inference of subscript on special form), <class 'object'>]
|
||||
```
|
||||
|
||||
### Not parameterized
|
||||
@@ -88,5 +88,5 @@ from typing_extensions import Annotated
|
||||
# error: [invalid-base]
|
||||
class C(Annotated): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ from typing import Any
|
||||
|
||||
class SubclassOfAny(Any): ...
|
||||
|
||||
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]]
|
||||
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[<class 'SubclassOfAny'>, Any, <class 'object'>]
|
||||
|
||||
x: SubclassOfAny = 1 # error: [invalid-assignment]
|
||||
y: int = SubclassOfAny()
|
||||
|
||||
190
crates/ty_python_semantic/resources/mdtest/annotations/self.md
Normal file
190
crates/ty_python_semantic/resources/mdtest/annotations/self.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Self
|
||||
|
||||
`Self` is treated as if it were a `TypeVar` bound to the class it's being used on.
|
||||
|
||||
`typing.Self` is only available in Python 3.11 and later.
|
||||
|
||||
## Methods
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class Shape:
|
||||
def set_scale(self: Self, scale: float) -> Self:
|
||||
reveal_type(self) # revealed: Self
|
||||
return self
|
||||
|
||||
def nested_type(self) -> list[Self]:
|
||||
return [self]
|
||||
|
||||
def nested_func(self: Self) -> Self:
|
||||
def inner() -> Self:
|
||||
reveal_type(self) # revealed: Self
|
||||
return self
|
||||
return inner()
|
||||
|
||||
def implicit_self(self) -> Self:
|
||||
# TODO: first argument in a method should be considered as "typing.Self"
|
||||
reveal_type(self) # revealed: Unknown
|
||||
return self
|
||||
|
||||
reveal_type(Shape().nested_type()) # revealed: @Todo(specialized non-generic class)
|
||||
reveal_type(Shape().nested_func()) # revealed: Shape
|
||||
|
||||
class Circle(Shape):
|
||||
def set_scale(self: Self, scale: float) -> Self:
|
||||
reveal_type(self) # revealed: Self
|
||||
return self
|
||||
|
||||
class Outer:
|
||||
class Inner:
|
||||
def foo(self: Self) -> Self:
|
||||
reveal_type(self) # revealed: Self
|
||||
return self
|
||||
```
|
||||
|
||||
## Class Methods
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, TypeVar
|
||||
|
||||
class Shape:
|
||||
def foo(self: Self) -> Self:
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def bar(cls: type[Self]) -> Self:
|
||||
# TODO: type[Shape]
|
||||
reveal_type(cls) # revealed: @Todo(unsupported type[X] special form)
|
||||
return cls()
|
||||
|
||||
class Circle(Shape): ...
|
||||
|
||||
reveal_type(Shape().foo()) # revealed: Shape
|
||||
# TODO: Shape
|
||||
reveal_type(Shape.bar()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class LinkedList:
|
||||
value: int
|
||||
next_node: Self
|
||||
|
||||
def next(self: Self) -> Self:
|
||||
reveal_type(self.value) # revealed: int
|
||||
return self.next_node
|
||||
|
||||
reveal_type(LinkedList().next()) # revealed: LinkedList
|
||||
```
|
||||
|
||||
## Generic Classes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Container(Generic[T]):
|
||||
value: T
|
||||
def set_value(self: Self, value: T) -> Self:
|
||||
return self
|
||||
|
||||
int_container: Container[int] = Container[int]()
|
||||
reveal_type(int_container) # revealed: Container[int]
|
||||
reveal_type(int_container.set_value(1)) # revealed: Container[int]
|
||||
```
|
||||
|
||||
## Protocols
|
||||
|
||||
TODO: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>
|
||||
|
||||
## Annotations
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class Shape:
|
||||
def union(self: Self, other: Self | None):
|
||||
reveal_type(other) # revealed: Self | None
|
||||
return self
|
||||
```
|
||||
|
||||
## Invalid Usage
|
||||
|
||||
`Self` cannot be used in the signature of a function or variable.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def x(s: Self): ...
|
||||
|
||||
# error: [invalid-type-form]
|
||||
b: Self
|
||||
|
||||
# TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self"
|
||||
class Foo:
|
||||
# TODO: rejected Self because self has a different type
|
||||
def has_existing_self_annotation(self: T) -> Self:
|
||||
return self # error: [invalid-return-type]
|
||||
|
||||
def return_concrete_type(self) -> Self:
|
||||
# TODO: tell user to use "Foo" instead of "Self"
|
||||
# error: [invalid-return-type]
|
||||
return Foo()
|
||||
|
||||
@staticmethod
|
||||
# TODO: reject because of staticmethod
|
||||
def make() -> Self:
|
||||
# error: [invalid-return-type]
|
||||
return Foo()
|
||||
|
||||
class Bar(Generic[T]):
|
||||
foo: T
|
||||
def bar(self) -> T:
|
||||
return self.foo
|
||||
|
||||
# error: [invalid-type-form]
|
||||
class Baz(Bar[Self]): ...
|
||||
|
||||
class MyMetaclass(type):
|
||||
# TODO: rejected
|
||||
def __new__(cls) -> Self:
|
||||
return super().__new__(cls)
|
||||
```
|
||||
@@ -85,25 +85,25 @@ import typing
|
||||
class ListSubclass(typing.List): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'ListSubclass'>, <class 'list'>, <class 'MutableSequence'>, <class 'Sequence'>, <class 'Reversible'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(ListSubclass.__mro__)
|
||||
|
||||
class DictSubclass(typing.Dict): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[DictSubclass], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
# revealed: tuple[<class 'DictSubclass'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], <class 'object'>]
|
||||
reveal_type(DictSubclass.__mro__)
|
||||
|
||||
class SetSubclass(typing.Set): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'SetSubclass'>, <class 'set'>, <class 'MutableSet'>, <class 'AbstractSet'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(SetSubclass.__mro__)
|
||||
|
||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]]
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset'>, <class 'AbstractSet'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(FrozenSetSubclass.__mro__)
|
||||
|
||||
####################
|
||||
@@ -113,30 +113,30 @@ reveal_type(FrozenSetSubclass.__mro__)
|
||||
class ChainMapSubclass(typing.ChainMap): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
# revealed: tuple[<class 'ChainMapSubclass'>, <class 'ChainMap[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], <class 'object'>]
|
||||
reveal_type(ChainMapSubclass.__mro__)
|
||||
|
||||
class CounterSubclass(typing.Counter): ...
|
||||
|
||||
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[CounterSubclass], Literal[Counter[Unknown]], Literal[dict[_T, int]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], Literal[object]]
|
||||
# revealed: tuple[<class 'CounterSubclass'>, <class 'Counter[Unknown]'>, <class 'dict[Unknown, int]'>, <class 'MutableMapping[Unknown, int]'>, <class 'Mapping[Unknown, int]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], <class 'object'>]
|
||||
reveal_type(CounterSubclass.__mro__)
|
||||
|
||||
class DefaultDictSubclass(typing.DefaultDict): ...
|
||||
|
||||
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
# revealed: tuple[<class 'DefaultDictSubclass'>, <class 'defaultdict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], <class 'object'>]
|
||||
reveal_type(DefaultDictSubclass.__mro__)
|
||||
|
||||
class DequeSubclass(typing.Deque): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'DequeSubclass'>, <class 'deque'>, <class 'MutableSequence'>, <class 'Sequence'>, <class 'Reversible'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(DequeSubclass.__mro__)
|
||||
|
||||
class OrderedDictSubclass(typing.OrderedDict): ...
|
||||
|
||||
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
# revealed: tuple[<class 'OrderedDictSubclass'>, <class 'OrderedDict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], <class 'object'>]
|
||||
reveal_type(OrderedDictSubclass.__mro__)
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user