Compare commits
41 Commits
cjm/iftc
...
brent/cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cabdd969ec | ||
|
|
2e5c8b9799 | ||
|
|
e1219bc27c | ||
|
|
926e83323a | ||
|
|
5cace28c3e | ||
|
|
3785e13231 | ||
|
|
c2380fa0e2 | ||
|
|
4dec44ae49 | ||
|
|
b6579eaf04 | ||
|
|
f063c0e874 | ||
|
|
6a65734ee3 | ||
|
|
00066e094c | ||
|
|
37a1958374 | ||
|
|
2535d791ae | ||
|
|
05c4399e7b | ||
|
|
b18434b0f6 | ||
|
|
17779c9a17 | ||
|
|
53fc0614da | ||
|
|
59249f483b | ||
|
|
84e76f4d04 | ||
|
|
0acc273286 | ||
|
|
93a9fabb26 | ||
|
|
98d1811dd1 | ||
|
|
06f9f52e59 | ||
|
|
e9a64e5825 | ||
|
|
360eb7005f | ||
|
|
630c7a3152 | ||
|
|
e6e029a8b7 | ||
|
|
64f9481fd0 | ||
|
|
99d0ac60b4 | ||
|
|
ba7ed3a6f9 | ||
|
|
39b41838f3 | ||
|
|
c7640a433e | ||
|
|
1765014be3 | ||
|
|
997dc2e7cc | ||
|
|
4aee0398cb | ||
|
|
1fd9103e81 | ||
|
|
ee2759b365 | ||
|
|
35f33d9bf5 | ||
|
|
5d78b3117a | ||
|
|
c2a05b4825 |
22
.github/workflows/ci.yaml
vendored
22
.github/workflows/ci.yaml
vendored
@@ -238,13 +238,13 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -296,13 +296,13 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -381,7 +381,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- name: "Build"
|
||||
run: cargo build --release --locked
|
||||
|
||||
@@ -406,7 +406,7 @@ jobs:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: rustup default "${MSRV}"
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- name: "Build tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -903,7 +903,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -936,7 +936,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- name: Build ruff
|
||||
# A debug build means the script runs slower once it gets started,
|
||||
|
||||
28
.github/workflows/sync_typeshed.yaml
vendored
28
.github/workflows/sync_typeshed.yaml
vendored
@@ -34,6 +34,10 @@ env:
|
||||
# and which all three workers push to.
|
||||
UPSTREAM_BRANCH: typeshedbot/sync-typeshed
|
||||
|
||||
# The path to the directory that contains the vendored typeshed stubs,
|
||||
# relative to the root of the Ruff repository.
|
||||
VENDORED_TYPESHED: crates/ty_vendored/vendor/typeshed
|
||||
|
||||
jobs:
|
||||
# Sync typeshed stubs, and sync all docstrings available on Linux.
|
||||
# Push the changes to a new branch on the upstream repository.
|
||||
@@ -64,20 +68,20 @@ jobs:
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- name: Sync typeshed stubs
|
||||
run: |
|
||||
rm -rf ruff/crates/ty_vendored/vendor/typeshed
|
||||
mkdir ruff/crates/ty_vendored/vendor/typeshed
|
||||
cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed
|
||||
cp typeshed/LICENSE ruff/crates/ty_vendored/vendor/typeshed
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}"
|
||||
mkdir "ruff/${VENDORED_TYPESHED}"
|
||||
cp typeshed/README.md "ruff/${VENDORED_TYPESHED}"
|
||||
cp typeshed/LICENSE "ruff/${VENDORED_TYPESHED}"
|
||||
|
||||
# The pyproject.toml file is needed by a later job for the black configuration.
|
||||
# It's deleted before creating the PR.
|
||||
cp typeshed/pyproject.toml ruff/crates/ty_vendored/vendor/typeshed
|
||||
cp typeshed/pyproject.toml "ruff/${VENDORED_TYPESHED}"
|
||||
|
||||
cp -r typeshed/stdlib ruff/crates/ty_vendored/vendor/typeshed/stdlib
|
||||
rm -rf ruff/crates/ty_vendored/vendor/typeshed/stdlib/@tests
|
||||
git -C typeshed rev-parse HEAD > ruff/crates/ty_vendored/vendor/typeshed/source_commit.txt
|
||||
cp -r typeshed/stdlib "ruff/${VENDORED_TYPESHED}/stdlib"
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}/stdlib/@tests"
|
||||
git -C typeshed rev-parse HEAD > "ruff/${VENDORED_TYPESHED}/source_commit.txt"
|
||||
cd ruff
|
||||
git checkout -b typeshedbot/sync-typeshed
|
||||
git checkout -b "${UPSTREAM_BRANCH}"
|
||||
git add .
|
||||
git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)" --allow-empty
|
||||
- name: Sync Linux docstrings
|
||||
@@ -167,17 +171,17 @@ jobs:
|
||||
# consistent with the other typeshed stubs around them.
|
||||
# Typeshed formats code using black in their CI, so we just invoke
|
||||
# black on the stubs the same way that typeshed does.
|
||||
uvx black crates/ty_vendored/vendor/typeshed/stdlib --config crates/ty_vendored/vendor/typeshed/pyproject.toml || true
|
||||
uvx black "${VENDORED_TYPESHED}/stdlib" --config "${VENDORED_TYPESHED}/pyproject.toml" || true
|
||||
git commit -am "Format codemodded docstrings" --allow-empty
|
||||
|
||||
rm crates/ty_vendored/vendor/typeshed/pyproject.toml
|
||||
rm "${VENDORED_TYPESHED}/pyproject.toml"
|
||||
git commit -am "Remove pyproject.toml file"
|
||||
|
||||
git push
|
||||
- name: Create a PR
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
|
||||
gh pr list --repo "${GITHUB_REPOSITORY}" --head "${UPSTREAM_BRANCH}" --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
|
||||
gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"
|
||||
|
||||
create-issue-on-failure:
|
||||
|
||||
@@ -81,7 +81,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.3
|
||||
rev: v0.12.4
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,5 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-type-checking`, `pyupgrade`, `ruff`\] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) ([#19100](https://github.com/astral-sh/ruff/pull/19100))
|
||||
- \[`flake8-use-pathlib`\] Add autofix for `PTH109` ([#19245](https://github.com/astral-sh/ruff/pull/19245))
|
||||
- \[`pylint`\] Detect indirect `pathlib.Path` usages for `unspecified-encoding` (`PLW1514`) ([#19304](https://github.com/astral-sh/ruff/pull/19304))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-bugbear`\] Fix `B017` false negatives for keyword exception arguments ([#19217](https://github.com/astral-sh/ruff/pull/19217))
|
||||
- \[`flake8-use-pathlib`\] Fix false negative on direct `Path()` instantiation (`PTH210`) ([#19388](https://github.com/astral-sh/ruff/pull/19388))
|
||||
- \[`flake8-django`\] Fix `DJ008` false positive for abstract models with type-annotated `abstract` field ([#19221](https://github.com/astral-sh/ruff/pull/19221))
|
||||
- \[`isort`\] Fix `I002` import insertion after docstring with multiple string statements ([#19222](https://github.com/astral-sh/ruff/pull/19222))
|
||||
- \[`isort`\] Treat form feed as valid whitespace before a semicolon ([#19343](https://github.com/astral-sh/ruff/pull/19343))
|
||||
- \[`pydoclint`\] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) ([#19246](https://github.com/astral-sh/ruff/pull/19246))
|
||||
- \[`refurb`\] `FURB164` fix should validate arguments and should usually be marked unsafe ([#19136](https://github.com/astral-sh/ruff/pull/19136))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-use-pathlib`\] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 ([#19331](https://github.com/astral-sh/ruff/pull/19331))
|
||||
- \[`pep8_naming`\] Avoid false positives on standard library functions with uppercase names (`N802`) ([#18907](https://github.com/astral-sh/ruff/pull/18907))
|
||||
- \[`pycodestyle`\] Handle brace escapes for t-strings in logical lines ([#19358](https://github.com/astral-sh/ruff/pull/19358))
|
||||
- \[`pylint`\] Extend invalid string character rules to include t-strings ([#19355](https://github.com/astral-sh/ruff/pull/19355))
|
||||
- \[`ruff`\] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ ([#19333](https://github.com/astral-sh/ruff/pull/19333))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`flake8-type-checking`\] Make `TC010` docs example more realistic ([#19356](https://github.com/astral-sh/ruff/pull/19356))
|
||||
- Make more documentation examples error out-of-the-box ([#19288](https://github.com/astral-sh/ruff/pull/19288),[#19272](https://github.com/astral-sh/ruff/pull/19272),[#19291](https://github.com/astral-sh/ruff/pull/19291),[#19296](https://github.com/astral-sh/ruff/pull/19296),[#19292](https://github.com/astral-sh/ruff/pull/19292),[#19295](https://github.com/astral-sh/ruff/pull/19295),[#19297](https://github.com/astral-sh/ruff/pull/19297),[#19309](https://github.com/astral-sh/ruff/pull/19309))
|
||||
|
||||
## 0.12.3
|
||||
|
||||
### Preview features
|
||||
|
||||
67
Cargo.lock
generated
67
Cargo.lock
generated
@@ -1557,6 +1557,15 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
@@ -2123,16 +2132,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "papaya"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"seize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
@@ -2562,9 +2561,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
@@ -2711,7 +2710,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2839,6 +2838,7 @@ dependencies = [
|
||||
"insta",
|
||||
"matchit",
|
||||
"path-slash",
|
||||
"quick-junit",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
"ruff_diagnostics",
|
||||
@@ -2962,7 +2962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -2987,7 +2987,6 @@ dependencies = [
|
||||
"pathdiff",
|
||||
"pep440_rs",
|
||||
"pyproject-toml",
|
||||
"quick-junit",
|
||||
"regex",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
@@ -3042,7 +3041,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"ruff_diagnostics",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
@@ -3295,7 +3294,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3409,7 +3408,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3419,7 +3418,7 @@ dependencies = [
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"intrusive-collections",
|
||||
"papaya",
|
||||
"inventory",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"rayon",
|
||||
@@ -3434,12 +3433,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3492,16 +3491,6 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "seize"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4b8d813387d566f627f3ea1b914c068aac94c40ae27ec43f5f33bde65abefe7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
@@ -3546,9 +3535,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
version = "1.0.141"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3738,23 +3727,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.1"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.1"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -4300,6 +4288,7 @@ name = "ty_server"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"crossbeam",
|
||||
"jod-thread",
|
||||
"libc",
|
||||
@@ -4574,7 +4563,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"uuid-macro-internal",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -138,7 +138,7 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "fc00eba89e5dcaa5edba51c41aa5f309b5cb126b" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "dba66f1a37acca014c2402f231ed5b361bd7d8fe" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -150,7 +150,7 @@ serde_with = { version = "3.6.0", default-features = false, features = [
|
||||
] }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.4.0", features = ["inline"] }
|
||||
smallvec = { version = "1.13.2" }
|
||||
smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new"] }
|
||||
snapbox = { version = "0.6.0", features = [
|
||||
"diff",
|
||||
"term-svg",
|
||||
|
||||
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.12.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.3/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.12.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.4/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.3
|
||||
rev: v0.12.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -18,14 +18,12 @@ use rustc_hash::FxHashMap;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use ruff_cache::{CacheKey, CacheKeyHasher};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Diagnostic, SerializableDiagnostics};
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_linter::message::create_lint_diagnostic;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::{VERSION, warn_user};
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::Settings;
|
||||
use ruff_workspace::resolver::Resolver;
|
||||
@@ -345,22 +343,7 @@ impl FileCache {
|
||||
let diagnostics = if lint.messages.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
|
||||
lint.messages
|
||||
.iter()
|
||||
.map(|msg| {
|
||||
create_lint_diagnostic(
|
||||
&msg.body,
|
||||
msg.suggestion.as_ref(),
|
||||
msg.range,
|
||||
msg.fix.clone(),
|
||||
msg.parent,
|
||||
file.clone(),
|
||||
msg.noqa_offset,
|
||||
msg.rule,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
lint.messages.to_diagnostics()
|
||||
};
|
||||
let notebook_indexes = if let Some(notebook_index) = lint.notebook_index.as_ref() {
|
||||
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
|
||||
@@ -415,7 +398,8 @@ pub(crate) struct LintCacheData {
|
||||
/// Imports made.
|
||||
// pub(super) imports: ImportMap,
|
||||
/// Diagnostic messages.
|
||||
pub(super) messages: Vec<CacheMessage>,
|
||||
#[bincode(with_serde)]
|
||||
pub(super) messages: SerializableDiagnostics,
|
||||
/// Source code of the file.
|
||||
///
|
||||
/// # Notes
|
||||
@@ -438,30 +422,7 @@ impl LintCacheData {
|
||||
String::new() // No messages, no need to keep the source!
|
||||
};
|
||||
|
||||
let messages = diagnostics
|
||||
.iter()
|
||||
// Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
|
||||
// this also serves to filter them out, but we shouldn't be caching files with syntax
|
||||
// errors anyway.
|
||||
.filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
|
||||
.map(|(rule, msg)| {
|
||||
// Make sure that all message use the same source file.
|
||||
assert_eq!(
|
||||
msg.expect_ruff_source_file(),
|
||||
diagnostics.first().unwrap().expect_ruff_source_file(),
|
||||
"message uses a different source file"
|
||||
);
|
||||
CacheMessage {
|
||||
rule,
|
||||
body: msg.body().to_string(),
|
||||
suggestion: msg.suggestion().map(ToString::to_string),
|
||||
range: msg.expect_range(),
|
||||
parent: msg.parent(),
|
||||
fix: msg.fix().cloned(),
|
||||
noqa_offset: msg.noqa_offset(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let messages = SerializableDiagnostics::new(diagnostics);
|
||||
|
||||
Self {
|
||||
messages,
|
||||
|
||||
@@ -15,8 +15,8 @@ use ruff_db::diagnostic::{
|
||||
use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::message::{
|
||||
Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, JunitEmitter,
|
||||
SarifEmitter, TextEmitter,
|
||||
Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, SarifEmitter,
|
||||
TextEmitter,
|
||||
};
|
||||
use ruff_linter::notify_user;
|
||||
use ruff_linter::settings::flags::{self};
|
||||
@@ -252,7 +252,11 @@ impl Printer {
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Junit => {
|
||||
JunitEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Junit)
|
||||
.preview(preview);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Concise | OutputFormat::Full => {
|
||||
TextEmitter::default()
|
||||
|
||||
@@ -5718,8 +5718,11 @@ match 42: # invalid-syntax
|
||||
|
||||
let snapshot = format!("output_format_{output_format}");
|
||||
|
||||
let project_dir = dunce::canonicalize(tempdir.path())?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![
|
||||
(tempdir_filter(&project_dir).as_str(), "[TMP]/"),
|
||||
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
|
||||
(r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#),
|
||||
(ruff_linter::VERSION, "[VERSION]"),
|
||||
|
||||
@@ -25,7 +25,7 @@ exit_code: 1
|
||||
<testcase name="org.ruff.F821" classname="[TMP]/input" line="2" column="5">
|
||||
<failure message="Undefined name `y`">line 2, col 5, Undefined name `y`</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff" classname="[TMP]/input" line="3" column="1">
|
||||
<testcase name="org.ruff.invalid-syntax" classname="[TMP]/input" line="3" column="1">
|
||||
<failure message="SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">line 3, col 1, SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)</failure>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
|
||||
@@ -18,7 +18,7 @@ use ruff_python_ast::PythonVersion;
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use ty_project::watch::{ChangeEvent, ChangedKind};
|
||||
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
|
||||
use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata};
|
||||
|
||||
struct Case {
|
||||
db: ProjectDatabase,
|
||||
@@ -102,6 +102,7 @@ fn setup_tomllib_case() -> Case {
|
||||
|
||||
let re = re.unwrap();
|
||||
|
||||
db.set_check_mode(CheckMode::OpenFiles);
|
||||
db.project().set_open_files(&mut db, tomllib_files);
|
||||
|
||||
let re_path = re.path(&db).as_system_path().unwrap().to_owned();
|
||||
@@ -237,6 +238,7 @@ fn setup_micro_case(code: &str) -> Case {
|
||||
let mut db = ProjectDatabase::new(metadata, system).unwrap();
|
||||
let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap();
|
||||
|
||||
db.set_check_mode(CheckMode::OpenFiles);
|
||||
db.project()
|
||||
.set_open_files(&mut db, FxHashSet::from_iter([file]));
|
||||
|
||||
@@ -525,14 +527,21 @@ impl<'a> ProjectBenchmark<'a> {
|
||||
|
||||
#[track_caller]
|
||||
fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) {
|
||||
fn check_project(db: &mut ProjectDatabase, max_diagnostics: usize) {
|
||||
fn check_project(db: &mut ProjectDatabase, project_name: &str, max_diagnostics: usize) {
|
||||
let result = db.check();
|
||||
let diagnostics = result.len();
|
||||
|
||||
assert!(
|
||||
diagnostics <= max_diagnostics,
|
||||
"Expected <={max_diagnostics} diagnostics but got {diagnostics}"
|
||||
);
|
||||
if diagnostics > max_diagnostics {
|
||||
let details = result
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.concise_message().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n ");
|
||||
assert!(
|
||||
diagnostics <= max_diagnostics,
|
||||
"{project_name}: Expected <={max_diagnostics} diagnostics but got {diagnostics}:\n {details}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setup_rayon();
|
||||
@@ -542,7 +551,7 @@ fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) {
|
||||
group.bench_function(benchmark.project.config.name, |b| {
|
||||
b.iter_batched_ref(
|
||||
|| benchmark.setup_iteration(),
|
||||
|db| check_project(db, benchmark.max_diagnostics),
|
||||
|db| check_project(db, benchmark.project.config.name, benchmark.max_diagnostics),
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
@@ -610,7 +619,7 @@ fn datetype(criterion: &mut Criterion) {
|
||||
max_dep_date: "2025-07-04",
|
||||
python_version: PythonVersion::PY313,
|
||||
},
|
||||
0,
|
||||
2,
|
||||
);
|
||||
|
||||
bench_project(&benchmark, criterion);
|
||||
|
||||
@@ -34,6 +34,7 @@ glob = { workspace = true }
|
||||
ignore = { workspace = true, optional = true }
|
||||
matchit = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
quick-junit = { workspace = true, optional = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
@@ -56,6 +57,7 @@ tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
cache = ["ruff_cache"]
|
||||
junit = ["dep:quick-junit"]
|
||||
os = ["ignore", "dep:etcetera"]
|
||||
serde = ["camino/serde1", "dep:serde", "dep:serde_json", "ruff_diagnostics/serde"]
|
||||
# Exposes testing utilities.
|
||||
|
||||
@@ -9,6 +9,9 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
|
||||
use crate::{Db, files::File};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub use serde_diagnostics::SerializableDiagnostics;
|
||||
|
||||
mod render;
|
||||
mod stylesheet;
|
||||
|
||||
@@ -790,6 +793,7 @@ impl Annotation {
|
||||
/// These tags are used to provide additional information about the annotation.
|
||||
/// and are passed through to the language server protocol.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum DiagnosticTag {
|
||||
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
|
||||
Unnecessary,
|
||||
@@ -804,6 +808,7 @@ pub enum DiagnosticTag {
|
||||
///
|
||||
/// Rules use kebab case, e.g. `no-foo`.
|
||||
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct LintName(&'static str);
|
||||
|
||||
impl LintName {
|
||||
@@ -844,6 +849,7 @@ impl PartialEq<&str> for LintName {
|
||||
|
||||
/// Uniquely identifies the kind of a diagnostic.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum DiagnosticId {
|
||||
Panic,
|
||||
|
||||
@@ -1141,6 +1147,7 @@ impl From<crate::files::FileRange> for Span {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
@@ -1282,6 +1289,9 @@ pub enum DiagnosticFormat {
|
||||
Rdjson,
|
||||
/// Print diagnostics in the format emitted by Pylint.
|
||||
Pylint,
|
||||
/// Print diagnostics in the format expected by JUnit.
|
||||
#[cfg(feature = "junit")]
|
||||
Junit,
|
||||
}
|
||||
|
||||
/// A representation of the kinds of messages inside a diagnostic.
|
||||
@@ -1341,6 +1351,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
|
||||
/// a blanket trait implementation for `IntoDiagnosticMessage` for
|
||||
/// anything that implements `std::fmt::Display`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct DiagnosticMessage(Box<str>);
|
||||
|
||||
impl DiagnosticMessage {
|
||||
@@ -1404,7 +1415,11 @@ impl<T: std::fmt::Display> IntoDiagnosticMessage for T {
|
||||
///
|
||||
/// For Ruff rules this means the noqa code.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(transparent)
|
||||
)]
|
||||
pub struct SecondaryCode(String);
|
||||
|
||||
impl SecondaryCode {
|
||||
@@ -1449,3 +1464,205 @@ impl From<&SecondaryCode> for SecondaryCode {
|
||||
value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_diagnostics {
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_source_file::{SourceFile, SourceFileBuilder};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
Annotation, Diagnostic, DiagnosticId, DiagnosticInner, DiagnosticMessage, DiagnosticTag,
|
||||
LintName, SecondaryCode, Severity, Span, SubDiagnostic, SubDiagnosticInner, UnifiedFile,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SerializableDiagnostics {
|
||||
source_files: FxHashMap<String, String>,
|
||||
diagnostics: Vec<SerializableDiagnostic>,
|
||||
}
|
||||
|
||||
impl SerializableDiagnostics {
|
||||
pub fn new(diagnostics: &[Diagnostic]) -> Self {
|
||||
let mut source_files = FxHashMap::default();
|
||||
let mut serializable_diagnostics = Vec::with_capacity(diagnostics.len());
|
||||
for diagnostic in diagnostics {
|
||||
let mut subs = Vec::with_capacity(diagnostic.inner.subs.len());
|
||||
for sub in &diagnostic.inner.subs {
|
||||
subs.push(SerializableSubDiagnostic {
|
||||
severity: sub.inner.severity,
|
||||
message: sub.inner.message.clone(),
|
||||
annotations: serializable_annotations(
|
||||
&mut source_files,
|
||||
&sub.inner.annotations,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
serializable_diagnostics.push(SerializableDiagnostic {
|
||||
id: diagnostic.inner.id,
|
||||
severity: diagnostic.inner.severity,
|
||||
message: diagnostic.inner.message.clone(),
|
||||
annotations: serializable_annotations(
|
||||
&mut source_files,
|
||||
&diagnostic.inner.annotations,
|
||||
),
|
||||
subs,
|
||||
fix: diagnostic.inner.fix.clone(),
|
||||
parent: diagnostic.inner.parent,
|
||||
noqa_offset: diagnostic.inner.noqa_offset,
|
||||
secondary_code: diagnostic.inner.secondary_code.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
source_files,
|
||||
diagnostics: serializable_diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.diagnostics.is_empty()
|
||||
}
|
||||
|
||||
pub fn to_diagnostics(&self) -> Vec<Diagnostic> {
|
||||
let source_files: FxHashMap<&str, SourceFile> = self
|
||||
.source_files
|
||||
.iter()
|
||||
.map(|(name, contents)| {
|
||||
(
|
||||
name.as_str(),
|
||||
SourceFileBuilder::new(name.clone(), contents.clone()).finish(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
self.diagnostics
|
||||
.iter()
|
||||
.map(|diag| Diagnostic {
|
||||
inner: Arc::new(DiagnosticInner {
|
||||
id: diag.id,
|
||||
severity: diag.severity,
|
||||
message: diag.message.clone(),
|
||||
annotations: annotations(&source_files, &diag.annotations),
|
||||
subs: subdiagnostics(&source_files, &diag.subs),
|
||||
fix: diag.fix.clone(),
|
||||
parent: diag.parent,
|
||||
noqa_offset: diag.noqa_offset,
|
||||
secondary_code: diag.secondary_code.clone(),
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn serializable_annotations(
|
||||
source_files: &mut FxHashMap<String, String>,
|
||||
annotations: &[Annotation],
|
||||
) -> Vec<SerializableAnnotation> {
|
||||
let mut serializable_annotations = Vec::with_capacity(annotations.len());
|
||||
for annotation in annotations {
|
||||
let file = annotation.span.expect_ruff_file();
|
||||
source_files.insert(file.name().to_string(), file.source_text().to_string());
|
||||
serializable_annotations.push(SerializableAnnotation {
|
||||
span: SerializableSpan {
|
||||
name: file.name().to_string(),
|
||||
range: annotation.span.range,
|
||||
},
|
||||
message: annotation.message.clone(),
|
||||
is_primary: annotation.is_primary,
|
||||
tags: annotation.tags.clone(),
|
||||
});
|
||||
}
|
||||
serializable_annotations
|
||||
}
|
||||
|
||||
fn annotations(
|
||||
source_files: &FxHashMap<&str, SourceFile>,
|
||||
serializable_annotations: &[SerializableAnnotation],
|
||||
) -> Vec<Annotation> {
|
||||
serializable_annotations
|
||||
.iter()
|
||||
.map(|ann| {
|
||||
let span = Span {
|
||||
file: UnifiedFile::Ruff(
|
||||
source_files
|
||||
.get(ann.span.name.as_str())
|
||||
.expect("Expected source file in cache")
|
||||
.clone(),
|
||||
),
|
||||
range: ann.span.range,
|
||||
};
|
||||
Annotation {
|
||||
span,
|
||||
message: ann.message.clone(),
|
||||
is_primary: ann.is_primary,
|
||||
tags: ann.tags.clone(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn subdiagnostics(
|
||||
source_files: &FxHashMap<&str, SourceFile>,
|
||||
serializable_annotations: &[SerializableSubDiagnostic],
|
||||
) -> Vec<SubDiagnostic> {
|
||||
serializable_annotations
|
||||
.iter()
|
||||
.map(|sub| SubDiagnostic {
|
||||
inner: Box::new(SubDiagnosticInner {
|
||||
severity: sub.severity,
|
||||
message: sub.message.clone(),
|
||||
annotations: annotations(source_files, &sub.annotations),
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SerializableDiagnostic {
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: DiagnosticMessage,
|
||||
annotations: Vec<SerializableAnnotation>,
|
||||
subs: Vec<SerializableSubDiagnostic>,
|
||||
fix: Option<Fix>,
|
||||
parent: Option<TextSize>,
|
||||
noqa_offset: Option<TextSize>,
|
||||
secondary_code: Option<SecondaryCode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SerializableAnnotation {
|
||||
span: SerializableSpan,
|
||||
message: Option<DiagnosticMessage>,
|
||||
is_primary: bool,
|
||||
tags: Vec<DiagnosticTag>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SerializableSubDiagnostic {
|
||||
severity: Severity,
|
||||
message: DiagnosticMessage,
|
||||
annotations: Vec<SerializableAnnotation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SerializableSpan {
|
||||
name: String,
|
||||
range: Option<TextRange>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for LintName {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?.into_boxed_str();
|
||||
Ok(LintName::of(Box::leak(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ mod azure;
|
||||
mod json;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json_lines;
|
||||
#[cfg(feature = "junit")]
|
||||
mod junit;
|
||||
mod pylint;
|
||||
#[cfg(feature = "serde")]
|
||||
mod rdjson;
|
||||
@@ -156,7 +158,8 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
AnnotateRenderer::styled()
|
||||
} else {
|
||||
AnnotateRenderer::plain()
|
||||
};
|
||||
}
|
||||
.cut_indicator("…");
|
||||
|
||||
renderer = renderer
|
||||
.error(stylesheet.error)
|
||||
@@ -196,6 +199,10 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
DiagnosticFormat::Pylint => {
|
||||
PylintRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
}
|
||||
#[cfg(feature = "junit")]
|
||||
DiagnosticFormat::Junit => {
|
||||
junit::JunitRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
195
crates/ruff_db/src/diagnostic/render/junit.rs
Normal file
195
crates/ruff_db/src/diagnostic/render/junit.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use std::{collections::BTreeMap, ops::Deref, path::Path};
|
||||
|
||||
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString};
|
||||
|
||||
use ruff_source_file::LineColumn;
|
||||
|
||||
use crate::diagnostic::{Diagnostic, SecondaryCode, render::FileResolver};
|
||||
|
||||
/// A renderer for diagnostics in the [JUnit] format.
|
||||
///
|
||||
/// See [`junit.xsd`] for the specification in the JUnit repository and an annotated [version]
|
||||
/// linked from the [`quick_junit`] docs.
|
||||
///
|
||||
/// [JUnit]: https://junit.org/
|
||||
/// [`junit.xsd`]: https://github.com/junit-team/junit-framework/blob/2870b7d8fd5bf7c1efe489d3991d3ed3900e82bb/platform-tests/src/test/resources/jenkins-junit.xsd
|
||||
/// [version]: https://llg.cubic.org/docs/junit/
|
||||
/// [`quick_junit`]: https://docs.rs/quick-junit/latest/quick_junit/
|
||||
pub struct JunitRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
}
|
||||
|
||||
impl<'a> JunitRenderer<'a> {
|
||||
pub fn new(resolver: &'a dyn FileResolver) -> Self {
|
||||
Self { resolver }
|
||||
}
|
||||
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
let mut report = Report::new("ruff");
|
||||
|
||||
if diagnostics.is_empty() {
|
||||
let mut test_suite = TestSuite::new("ruff");
|
||||
test_suite
|
||||
.extra
|
||||
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
|
||||
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
|
||||
case.set_classname("ruff");
|
||||
test_suite.add_test_case(case);
|
||||
report.add_test_suite(test_suite);
|
||||
} else {
|
||||
for (filename, diagnostics) in group_diagnostics_by_filename(diagnostics, self.resolver)
|
||||
{
|
||||
let mut test_suite = TestSuite::new(filename);
|
||||
test_suite
|
||||
.extra
|
||||
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
|
||||
|
||||
let classname = Path::new(filename).with_extension("");
|
||||
|
||||
for diagnostic in diagnostics {
|
||||
let DiagnosticWithLocation {
|
||||
diagnostic,
|
||||
start_location: location,
|
||||
} = diagnostic;
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(diagnostic.body());
|
||||
|
||||
if let Some(location) = location {
|
||||
status.set_description(format!(
|
||||
"line {row}, col {col}, {body}",
|
||||
row = location.line,
|
||||
col = location.column,
|
||||
body = diagnostic.body()
|
||||
));
|
||||
} else {
|
||||
status.set_description(diagnostic.body());
|
||||
}
|
||||
|
||||
let code = diagnostic
|
||||
.secondary_code()
|
||||
.map_or_else(|| diagnostic.name(), SecondaryCode::as_str);
|
||||
let mut case = TestCase::new(format!("org.ruff.{code}"), status);
|
||||
case.set_classname(classname.to_str().unwrap());
|
||||
|
||||
if let Some(location) = location {
|
||||
case.extra.insert(
|
||||
XmlString::new("line"),
|
||||
XmlString::new(location.line.to_string()),
|
||||
);
|
||||
case.extra.insert(
|
||||
XmlString::new("column"),
|
||||
XmlString::new(location.column.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
test_suite.add_test_case(case);
|
||||
}
|
||||
report.add_test_suite(test_suite);
|
||||
}
|
||||
}
|
||||
|
||||
let adapter = FmtAdapter { fmt: f };
|
||||
report.serialize(adapter).map_err(|_| std::fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(brent) this and `group_diagnostics_by_filename` are also used by the `grouped` output
|
||||
// format. I think they'd make more sense in that file, but I started here first. I'll move them to
|
||||
// that module when adding the `grouped` output format.
|
||||
struct DiagnosticWithLocation<'a> {
|
||||
diagnostic: &'a Diagnostic,
|
||||
start_location: Option<LineColumn>,
|
||||
}
|
||||
|
||||
impl Deref for DiagnosticWithLocation<'_> {
|
||||
type Target = Diagnostic;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.diagnostic
|
||||
}
|
||||
}
|
||||
|
||||
fn group_diagnostics_by_filename<'a>(
|
||||
diagnostics: &'a [Diagnostic],
|
||||
resolver: &'a dyn FileResolver,
|
||||
) -> BTreeMap<&'a str, Vec<DiagnosticWithLocation<'a>>> {
|
||||
let mut grouped_diagnostics = BTreeMap::default();
|
||||
for diagnostic in diagnostics {
|
||||
let (filename, start_location) = diagnostic
|
||||
.primary_span_ref()
|
||||
.map(|span| {
|
||||
let file = span.file();
|
||||
let start_location =
|
||||
span.range()
|
||||
.filter(|_| !resolver.is_notebook(file))
|
||||
.map(|range| {
|
||||
file.diagnostic_source(resolver)
|
||||
.as_source_code()
|
||||
.line_column(range.start())
|
||||
});
|
||||
|
||||
(span.file().path(resolver), start_location)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
grouped_diagnostics
|
||||
.entry(filename)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(DiagnosticWithLocation {
|
||||
diagnostic,
|
||||
start_location,
|
||||
});
|
||||
}
|
||||
grouped_diagnostics
|
||||
}
|
||||
|
||||
struct FmtAdapter<'a> {
|
||||
fmt: &'a mut dyn std::fmt::Write,
|
||||
}
|
||||
|
||||
impl std::io::Write for FmtAdapter<'_> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.fmt
|
||||
.write_str(std::str::from_utf8(buf).map_err(|_| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Invalid UTF-8 in JUnit report",
|
||||
)
|
||||
})?)
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
|
||||
self.fmt.write_fmt(args).map_err(std::io::Error::other)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Junit);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Junit);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/junit.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/junit.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites name="ruff" tests="3" failures="3" errors="0">
|
||||
@@ -1,15 +1,14 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/junit.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/junit.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites name="ruff" tests="2" failures="2" errors="0">
|
||||
<testsuite name="syntax_errors.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
|
||||
<testcase name="org.ruff" classname="syntax_errors" line="1" column="15">
|
||||
<testcase name="org.ruff.invalid-syntax" classname="syntax_errors" line="1" column="15">
|
||||
<failure message="SyntaxError: Expected one or more symbol names after import">line 1, col 15, SyntaxError: Expected one or more symbol names after import</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff" classname="syntax_errors" line="3" column="12">
|
||||
<testcase name="org.ruff.invalid-syntax" classname="syntax_errors" line="3" column="12">
|
||||
<failure message="SyntaxError: Expected ')', found newline">line 3, col 12, SyntaxError: Expected ')', found newline</failure>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
@@ -232,7 +232,7 @@ impl Files {
|
||||
let roots = inner.roots.read().unwrap();
|
||||
|
||||
for root in roots.all() {
|
||||
if root.path(db).starts_with(&path) {
|
||||
if path.starts_with(root.path(db)) {
|
||||
root.set_revision(db).to(FileRevision::now());
|
||||
}
|
||||
}
|
||||
@@ -375,12 +375,25 @@ impl File {
|
||||
}
|
||||
|
||||
/// Refreshes the file metadata by querying the file system if needed.
|
||||
///
|
||||
/// This also "touches" the file root associated with the given path.
|
||||
/// This means that any Salsa queries that depend on the corresponding
|
||||
/// root's revision will become invalidated.
|
||||
pub fn sync_path(db: &mut dyn Db, path: &SystemPath) {
|
||||
let absolute = SystemPath::absolute(path, db.system().current_directory());
|
||||
Files::touch_root(db, &absolute);
|
||||
Self::sync_system_path(db, &absolute, None);
|
||||
}
|
||||
|
||||
/// Refreshes *only* the file metadata by querying the file system if needed.
|
||||
///
|
||||
/// This specifically does not touch any file root associated with the
|
||||
/// given file path.
|
||||
pub fn sync_path_only(db: &mut dyn Db, path: &SystemPath) {
|
||||
let absolute = SystemPath::absolute(path, db.system().current_directory());
|
||||
Self::sync_system_path(db, &absolute, None);
|
||||
}
|
||||
|
||||
/// Increments the revision for the virtual file at `path`.
|
||||
pub fn sync_virtual_path(db: &mut dyn Db, path: &SystemVirtualPath) {
|
||||
if let Some(virtual_file) = db.files().try_virtual_file(path) {
|
||||
@@ -486,7 +499,7 @@ impl fmt::Debug for File {
|
||||
///
|
||||
/// This is a wrapper around a [`File`] that provides additional methods to interact with a virtual
|
||||
/// file.
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct VirtualFile(File);
|
||||
|
||||
impl VirtualFile {
|
||||
|
||||
@@ -23,7 +23,7 @@ pub struct FileRoot {
|
||||
pub path: SystemPathBuf,
|
||||
|
||||
/// The kind of the root at the time of its creation.
|
||||
kind_at_time_of_creation: FileRootKind,
|
||||
pub kind_at_time_of_creation: FileRootKind,
|
||||
|
||||
/// A revision that changes when the contents of the source root change.
|
||||
///
|
||||
|
||||
@@ -87,7 +87,7 @@ impl SourceDb for ModuleDb {
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for ModuleDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
fn should_check_file(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -15,7 +15,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ruff_annotate_snippets = { workspace = true }
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["serde"] }
|
||||
ruff_db = { workspace = true, features = ["junit", "serde"] }
|
||||
ruff_diagnostics = { workspace = true, features = ["serde"] }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
@@ -55,7 +55,6 @@ path-absolutize = { workspace = true, features = [
|
||||
pathdiff = { workspace = true }
|
||||
pep440_rs = { workspace = true }
|
||||
pyproject-toml = { workspace = true }
|
||||
quick-junit = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
|
||||
@@ -104,3 +104,6 @@ os.chmod(x)
|
||||
os.replace("src", "dst", src_dir_fd=1, dst_dir_fd=2)
|
||||
os.replace("src", "dst", src_dir_fd=1)
|
||||
os.replace("src", "dst", dst_dir_fd=2)
|
||||
|
||||
os.getcwd()
|
||||
os.getcwdb()
|
||||
@@ -79,3 +79,8 @@ def in_type_def():
|
||||
from typing import cast
|
||||
a = 'int'
|
||||
cast('f"{a}"','11')
|
||||
|
||||
# Regression test for parser bug
|
||||
# https://github.com/astral-sh/ruff/issues/18860
|
||||
def fuzz_bug():
|
||||
c('{\t"i}')
|
||||
|
||||
24
crates/ruff_linter/resources/test/fixtures/ruff/RUF055_3.py
vendored
Normal file
24
crates/ruff_linter/resources/test/fixtures/ruff/RUF055_3.py
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import re
|
||||
|
||||
b_src = b"abc"
|
||||
|
||||
# Should be replaced with `b_src.replace(rb"x", b"y")`
|
||||
re.sub(rb"x", b"y", b_src)
|
||||
|
||||
# Should be replaced with `b_src.startswith(rb"abc")`
|
||||
if re.match(rb"abc", b_src):
|
||||
pass
|
||||
|
||||
# Should be replaced with `rb"x" in b_src`
|
||||
if re.search(rb"x", b_src):
|
||||
pass
|
||||
|
||||
# Should be replaced with `b_src.split(rb"abc")`
|
||||
re.split(rb"abc", b_src)
|
||||
|
||||
# Patterns containing metacharacters should NOT be replaced
|
||||
re.sub(rb"ab[c]", b"", b_src)
|
||||
re.match(rb"ab[c]", b_src)
|
||||
re.search(rb"ab[c]", b_src)
|
||||
re.fullmatch(rb"ab[c]", b_src)
|
||||
re.split(rb"ab[c]", b_src)
|
||||
@@ -1044,7 +1044,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
Rule::OsMakedirs,
|
||||
Rule::OsRename,
|
||||
Rule::OsReplace,
|
||||
Rule::OsGetcwd,
|
||||
Rule::OsStat,
|
||||
Rule::OsPathJoin,
|
||||
Rule::OsPathSamefile,
|
||||
@@ -1110,6 +1109,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::OsReadlink) {
|
||||
flake8_use_pathlib::rules::os_readlink(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsGetcwd) {
|
||||
flake8_use_pathlib::rules::os_getcwd(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
|
||||
flake8_use_pathlib::rules::path_constructor_current_directory(
|
||||
checker, call, segments,
|
||||
|
||||
@@ -928,7 +928,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
|
||||
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
|
||||
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
|
||||
(Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsGetcwd),
|
||||
(Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsGetcwd),
|
||||
(Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExists),
|
||||
(Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExpanduser),
|
||||
(Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsdir),
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString};
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_source_file::LineColumn;
|
||||
|
||||
use crate::message::{Emitter, EmitterContext, MessageWithLocation, group_diagnostics_by_filename};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JunitEmitter;
|
||||
|
||||
impl Emitter for JunitEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
diagnostics: &[Diagnostic],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut report = Report::new("ruff");
|
||||
|
||||
if diagnostics.is_empty() {
|
||||
let mut test_suite = TestSuite::new("ruff");
|
||||
test_suite
|
||||
.extra
|
||||
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
|
||||
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
|
||||
case.set_classname("ruff");
|
||||
test_suite.add_test_case(case);
|
||||
report.add_test_suite(test_suite);
|
||||
} else {
|
||||
for (filename, messages) in group_diagnostics_by_filename(diagnostics) {
|
||||
let mut test_suite = TestSuite::new(&filename);
|
||||
test_suite
|
||||
.extra
|
||||
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
|
||||
|
||||
for message in messages {
|
||||
let MessageWithLocation {
|
||||
message,
|
||||
start_location,
|
||||
} = message;
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(message.body());
|
||||
let location = if context.is_notebook(&message.expect_ruff_filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
LineColumn::default()
|
||||
} else {
|
||||
start_location
|
||||
};
|
||||
|
||||
status.set_description(format!(
|
||||
"line {row}, col {col}, {body}",
|
||||
row = location.line,
|
||||
col = location.column,
|
||||
body = message.body()
|
||||
));
|
||||
let mut case = TestCase::new(
|
||||
if let Some(code) = message.secondary_code() {
|
||||
format!("org.ruff.{code}")
|
||||
} else {
|
||||
"org.ruff".to_string()
|
||||
},
|
||||
status,
|
||||
);
|
||||
let file_path = Path::new(&*filename);
|
||||
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
|
||||
let classname = file_path.parent().unwrap().join(file_stem);
|
||||
case.set_classname(classname.to_str().unwrap());
|
||||
case.extra.insert(
|
||||
XmlString::new("line"),
|
||||
XmlString::new(location.line.to_string()),
|
||||
);
|
||||
case.extra.insert(
|
||||
XmlString::new("column"),
|
||||
XmlString::new(location.column.to_string()),
|
||||
);
|
||||
|
||||
test_suite.add_test_case(case);
|
||||
}
|
||||
report.add_test_suite(test_suite);
|
||||
}
|
||||
}
|
||||
|
||||
report.serialize(writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::JunitEmitter;
|
||||
use crate::message::tests::{
|
||||
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = JunitEmitter;
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let mut emitter = JunitEmitter;
|
||||
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ use ruff_db::files::File;
|
||||
pub use github::GithubEmitter;
|
||||
pub use gitlab::GitlabEmitter;
|
||||
pub use grouped::GroupedEmitter;
|
||||
pub use junit::JunitEmitter;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::{LineColumn, SourceFile};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
@@ -28,7 +27,6 @@ mod diff;
|
||||
mod github;
|
||||
mod gitlab;
|
||||
mod grouped;
|
||||
mod junit;
|
||||
mod sarif;
|
||||
mod text;
|
||||
|
||||
|
||||
@@ -134,6 +134,11 @@ pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) ->
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19245
|
||||
pub(crate) const fn is_fix_os_getcwd_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/11436
|
||||
// https://github.com/astral-sh/ruff/pull/11168
|
||||
pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) use glob_rule::*;
|
||||
pub(crate) use invalid_pathlib_with_suffix::*;
|
||||
pub(crate) use os_getcwd::*;
|
||||
pub(crate) use os_path_abspath::*;
|
||||
pub(crate) use os_path_basename::*;
|
||||
pub(crate) use os_path_dirname::*;
|
||||
@@ -23,6 +24,7 @@ pub(crate) use replaceable_by_pathlib::*;
|
||||
|
||||
mod glob_rule;
|
||||
mod invalid_pathlib_with_suffix;
|
||||
mod os_getcwd;
|
||||
mod os_path_abspath;
|
||||
mod os_path_basename;
|
||||
mod os_path_dirname;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::preview::is_fix_os_getcwd_enabled;
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.getcwd` and `os.getcwdb`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`. When possible, using `Path` object
|
||||
/// methods such as `Path.cwd()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.getcwd()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// cwd = os.getcwd()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// cwd = Path.cwd()
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd)
|
||||
/// - [Python documentation: `os.getcwd`](https://docs.python.org/3/library/os.html#os.getcwd)
|
||||
/// - [Python documentation: `os.getcwdb`](https://docs.python.org/3/library/os.html#os.getcwdb)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsGetcwd;
|
||||
|
||||
impl Violation for OsGetcwd {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.getcwd()` should be replaced by `Path.cwd()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path.cwd()`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH109
|
||||
pub(crate) fn os_getcwd(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if !matches!(segments, ["os", "getcwd" | "getcwdb"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let range = call.range();
|
||||
let mut diagnostic = checker.report_diagnostic(OsGetcwd, call.func.range());
|
||||
|
||||
if !call.arguments.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if is_fix_os_getcwd_enabled(checker.settings()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import("pathlib", "Path"),
|
||||
call.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
let replacement = format!("{binding}.cwd()");
|
||||
|
||||
Ok(Fix::applicable_edits(
|
||||
Edit::range_replacement(replacement, range),
|
||||
[import_edit],
|
||||
applicability,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_use_pathlib::helpers::is_keyword_only_argument_non_default;
|
||||
use crate::rules::flake8_use_pathlib::rules::Glob;
|
||||
use crate::rules::flake8_use_pathlib::violations::{
|
||||
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathJoin,
|
||||
OsPathSamefile, OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath,
|
||||
BuiltinOpen, Joiner, OsChmod, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSamefile,
|
||||
OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath,
|
||||
};
|
||||
|
||||
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
@@ -83,10 +83,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsReplace, range)
|
||||
}
|
||||
// PTH109
|
||||
["os", "getcwd"] => checker.report_diagnostic_if_enabled(OsGetcwd, range),
|
||||
["os", "getcwdb"] => checker.report_diagnostic_if_enabled(OsGetcwd, range),
|
||||
|
||||
// PTH116
|
||||
["os", "stat"] => {
|
||||
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
|
||||
|
||||
@@ -103,6 +103,7 @@ full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
17 | b = os.path.exists(p)
|
||||
18 | bb = os.path.expanduser(p)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
full_name.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists()`
|
||||
|
|
||||
@@ -292,6 +293,7 @@ full_name.py:35:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
36 | os.path.join(p, *q)
|
||||
37 | os.sep.join(p, *q)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
full_name.py:36:1: PTH118 `os.path.join()` should be replaced by `Path.joinpath()`
|
||||
|
|
||||
@@ -360,3 +362,21 @@ full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()`
|
||||
72 |
|
||||
73 | # https://github.com/astral-sh/ruff/issues/17693
|
||||
|
|
||||
|
||||
full_name.py:108:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
|
|
||||
106 | os.replace("src", "dst", dst_dir_fd=2)
|
||||
107 |
|
||||
108 | os.getcwd()
|
||||
| ^^^^^^^^^ PTH109
|
||||
109 | os.getcwdb()
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
full_name.py:109:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
|
|
||||
108 | os.getcwd()
|
||||
109 | os.getcwdb()
|
||||
| ^^^^^^^^^^ PTH109
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
@@ -103,6 +103,7 @@ import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
17 | b = foo_p.exists(p)
|
||||
18 | bb = foo_p.expanduser(p)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
import_as.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists()`
|
||||
|
|
||||
|
||||
@@ -103,6 +103,7 @@ import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
19 | b = exists(p)
|
||||
20 | bb = expanduser(p)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
import_from.py:19:5: PTH110 `os.path.exists()` should be replaced by `Path.exists()`
|
||||
|
|
||||
|
||||
@@ -103,6 +103,7 @@ import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
24 | b = xexists(p)
|
||||
25 | bb = xexpanduser(p)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
import_from_as.py:24:5: PTH110 `os.path.exists()` should be replaced by `Path.exists()`
|
||||
|
|
||||
|
||||
@@ -168,6 +168,7 @@ full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
17 | b = os.path.exists(p)
|
||||
18 | bb = os.path.expanduser(p)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
full_name.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
||||
|
|
||||
@@ -510,6 +511,7 @@ full_name.py:35:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
36 | os.path.join(p, *q)
|
||||
37 | os.sep.join(p, *q)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
full_name.py:36:1: PTH118 `os.path.join()` should be replaced by `Path.joinpath()`
|
||||
|
|
||||
@@ -578,3 +580,50 @@ full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()`
|
||||
72 |
|
||||
73 | # https://github.com/astral-sh/ruff/issues/17693
|
||||
|
|
||||
|
||||
full_name.py:108:1: PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
|
|
||||
106 | os.replace("src", "dst", dst_dir_fd=2)
|
||||
107 |
|
||||
108 | os.getcwd()
|
||||
| ^^^^^^^^^ PTH109
|
||||
109 | os.getcwdb()
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | import os
|
||||
2 2 | import os.path
|
||||
3 |+import pathlib
|
||||
3 4 |
|
||||
4 5 | p = "/foo"
|
||||
5 6 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
105 106 | os.replace("src", "dst", src_dir_fd=1)
|
||||
106 107 | os.replace("src", "dst", dst_dir_fd=2)
|
||||
107 108 |
|
||||
108 |-os.getcwd()
|
||||
109 |+pathlib.Path.cwd()
|
||||
109 110 | os.getcwdb()
|
||||
|
||||
full_name.py:109:1: PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
|
|
||||
108 | os.getcwd()
|
||||
109 | os.getcwdb()
|
||||
| ^^^^^^^^^^ PTH109
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | import os
|
||||
2 2 | import os.path
|
||||
3 |+import pathlib
|
||||
3 4 |
|
||||
4 5 | p = "/foo"
|
||||
5 6 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
106 107 | os.replace("src", "dst", dst_dir_fd=2)
|
||||
107 108 |
|
||||
108 109 | os.getcwd()
|
||||
109 |-os.getcwdb()
|
||||
110 |+pathlib.Path.cwd()
|
||||
|
||||
@@ -168,6 +168,7 @@ import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
17 | b = foo_p.exists(p)
|
||||
18 | bb = foo_p.expanduser(p)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
import_as.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
||||
|
|
||||
|
||||
@@ -172,6 +172,7 @@ import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
19 | b = exists(p)
|
||||
20 | bb = expanduser(p)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
import_from.py:19:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
||||
|
|
||||
|
||||
@@ -172,6 +172,7 @@ import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
24 | b = xexists(p)
|
||||
25 | bb = xexpanduser(p)
|
||||
|
|
||||
= help: Replace with `Path.cwd()`
|
||||
|
||||
import_from_as.py:24:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
||||
|
|
||||
|
||||
@@ -230,52 +230,6 @@ impl Violation for OsReplace {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.getcwd` and `os.getcwdb`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`. When possible, using `Path` object
|
||||
/// methods such as `Path.cwd()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.getcwd()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// cwd = os.getcwd()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// cwd = Path.cwd()
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd)
|
||||
/// - [Python documentation: `os.getcwd`](https://docs.python.org/3/library/os.html#os.getcwd)
|
||||
/// - [Python documentation: `os.getcwdb`](https://docs.python.org/3/library/os.html#os.getcwdb)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsGetcwd;
|
||||
|
||||
impl Violation for OsGetcwd {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.getcwd()` should be replaced by `Path.cwd()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.stat`.
|
||||
///
|
||||
|
||||
@@ -534,6 +534,7 @@ mod tests {
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_0.py"))]
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))]
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_2.py"))]
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_3.py"))]
|
||||
#[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))]
|
||||
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
|
||||
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{
|
||||
Arguments, CmpOp, Expr, ExprAttribute, ExprCall, ExprCompare, ExprContext, ExprStringLiteral,
|
||||
ExprUnaryOp, Identifier, UnaryOp,
|
||||
Arguments, CmpOp, Expr, ExprAttribute, ExprBytesLiteral, ExprCall, ExprCompare, ExprContext,
|
||||
ExprStringLiteral, ExprUnaryOp, Identifier, UnaryOp,
|
||||
};
|
||||
use ruff_python_semantic::analyze::typing::find_binding_value;
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
@@ -72,6 +72,9 @@ impl Violation for UnnecessaryRegularExpression {
|
||||
}
|
||||
}
|
||||
|
||||
const METACHARACTERS: [char; 12] = ['.', '^', '$', '*', '+', '?', '{', '[', '\\', '|', '(', ')'];
|
||||
const ESCAPABLE_SINGLE_CHARACTERS: &str = "abfnrtv";
|
||||
|
||||
/// RUF055
|
||||
pub(crate) fn unnecessary_regular_expression(checker: &Checker, call: &ExprCall) {
|
||||
// adapted from unraw_re_pattern
|
||||
@@ -96,16 +99,19 @@ pub(crate) fn unnecessary_regular_expression(checker: &Checker, call: &ExprCall)
|
||||
};
|
||||
|
||||
// For now, restrict this rule to string literals and variables that can be resolved to literals
|
||||
let Some(string_lit) = resolve_string_literal(re_func.pattern, semantic) else {
|
||||
let Some(literal) = resolve_literal(re_func.pattern, semantic) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// For now, reject any regex metacharacters. Compare to the complete list
|
||||
// from https://docs.python.org/3/howto/regex.html#matching-characters
|
||||
let has_metacharacters = string_lit
|
||||
.value
|
||||
.to_str()
|
||||
.contains(['.', '^', '$', '*', '+', '?', '{', '[', '\\', '|', '(', ')']);
|
||||
let has_metacharacters = match &literal {
|
||||
Literal::Str(str_lit) => str_lit.value.to_str().contains(METACHARACTERS),
|
||||
Literal::Bytes(bytes_lit) => bytes_lit
|
||||
.value
|
||||
.iter()
|
||||
.any(|part| part.iter().any(|&b| METACHARACTERS.contains(&(b as char)))),
|
||||
};
|
||||
|
||||
if has_metacharacters {
|
||||
return;
|
||||
@@ -186,28 +192,48 @@ impl<'a> ReFunc<'a> {
|
||||
// version
|
||||
("sub", 3) => {
|
||||
let repl = call.arguments.find_argument_value("repl", 1)?;
|
||||
let lit = resolve_string_literal(repl, semantic)?;
|
||||
let lit = resolve_literal(repl, semantic)?;
|
||||
let mut fixable = true;
|
||||
for (c, next) in lit.value.chars().tuple_windows() {
|
||||
// `\0` (or any other ASCII digit) and `\g` have special meaning in `repl` strings.
|
||||
// Meanwhile, nearly all other escapes of ASCII letters in a `repl` string causes
|
||||
// `re.PatternError` to be raised at runtime.
|
||||
//
|
||||
// If we see that the escaped character is an alphanumeric ASCII character,
|
||||
// we should only emit a diagnostic suggesting to replace the `re.sub()` call with
|
||||
// `str.replace`if we can detect that the escaped character is one that is both
|
||||
// valid in a `repl` string *and* does not have any special meaning in a REPL string.
|
||||
//
|
||||
// It's out of scope for this rule to change invalid `re.sub()` calls into something
|
||||
// that would not raise an exception at runtime. They should be left as-is.
|
||||
if c == '\\' && next.is_ascii_alphanumeric() {
|
||||
if "abfnrtv".contains(next) {
|
||||
fixable = false;
|
||||
} else {
|
||||
return None;
|
||||
|
||||
match lit {
|
||||
Literal::Str(lit_str) => {
|
||||
// Perform escape analysis for replacement literals.
|
||||
for (c, next) in lit_str.value.to_str().chars().tuple_windows() {
|
||||
// `\\0` (or any other ASCII digit) and `\\g` have special meaning in `repl` strings.
|
||||
// Meanwhile, nearly all other escapes of ASCII letters in a `repl` string causes
|
||||
// `re.PatternError` to be raised at runtime.
|
||||
//
|
||||
// If we see that the escaped character is an alphanumeric ASCII character,
|
||||
// we should only emit a diagnostic suggesting to replace the `re.sub()` call with
|
||||
// `str.replace`if we can detect that the escaped character is one that is both
|
||||
// valid in a `repl` string *and* does not have any special meaning in a REPL string.
|
||||
//
|
||||
// It's out of scope for this rule to change invalid `re.sub()` calls into something
|
||||
// that would not raise an exception at runtime. They should be left as-is.
|
||||
if c == '\\' && next.is_ascii_alphanumeric() {
|
||||
if ESCAPABLE_SINGLE_CHARACTERS.contains(next) {
|
||||
fixable = false;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Literal::Bytes(lit_bytes) => {
|
||||
for part in &lit_bytes.value {
|
||||
for (byte, next) in part.iter().copied().tuple_windows() {
|
||||
if byte == b'\\' && (next as char).is_ascii_alphanumeric() {
|
||||
if ESCAPABLE_SINGLE_CHARACTERS.contains(next as char) {
|
||||
fixable = false;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(ReFunc {
|
||||
kind: ReFuncKind::Sub {
|
||||
repl: fixable.then_some(repl),
|
||||
@@ -329,6 +355,43 @@ impl<'a> ReFunc<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A literal that can be either a string or a bytes literal.
|
||||
enum Literal<'a> {
|
||||
Str(&'a ExprStringLiteral),
|
||||
Bytes(&'a ExprBytesLiteral),
|
||||
}
|
||||
|
||||
/// Try to resolve `name` to either a string or bytes literal in `semantic`.
|
||||
fn resolve_literal<'a>(name: &'a Expr, semantic: &'a SemanticModel) -> Option<Literal<'a>> {
|
||||
if let Some(str_lit) = resolve_string_literal(name, semantic) {
|
||||
return Some(Literal::Str(str_lit));
|
||||
}
|
||||
if let Some(bytes_lit) = resolve_bytes_literal(name, semantic) {
|
||||
return Some(Literal::Bytes(bytes_lit));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Try to resolve `name` to an [`ExprBytesLiteral`] in `semantic`.
|
||||
fn resolve_bytes_literal<'a>(
|
||||
name: &'a Expr,
|
||||
semantic: &'a SemanticModel,
|
||||
) -> Option<&'a ExprBytesLiteral> {
|
||||
if name.is_bytes_literal_expr() {
|
||||
return name.as_bytes_literal_expr();
|
||||
}
|
||||
|
||||
if let Some(name_expr) = name.as_name_expr() {
|
||||
let binding = semantic.binding(semantic.only_binding(name_expr)?);
|
||||
let value = find_binding_value(binding, semantic)?;
|
||||
if value.is_bytes_literal_expr() {
|
||||
return value.as_bytes_literal_expr();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Try to resolve `name` to an [`ExprStringLiteral`] in `semantic`.
|
||||
fn resolve_string_literal<'a>(
|
||||
name: &'a Expr,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF055_3.py:6:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
5 | # Should be replaced with `b_src.replace(rb"x", b"y")`
|
||||
6 | re.sub(rb"x", b"y", b_src)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
7 |
|
||||
8 | # Should be replaced with `b_src.startswith(rb"abc")`
|
||||
|
|
||||
= help: Replace with `b_src.replace(rb"x", b"y")`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | b_src = b"abc"
|
||||
4 4 |
|
||||
5 5 | # Should be replaced with `b_src.replace(rb"x", b"y")`
|
||||
6 |-re.sub(rb"x", b"y", b_src)
|
||||
6 |+b_src.replace(rb"x", b"y")
|
||||
7 7 |
|
||||
8 8 | # Should be replaced with `b_src.startswith(rb"abc")`
|
||||
9 9 | if re.match(rb"abc", b_src):
|
||||
|
||||
RUF055_3.py:9:4: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
8 | # Should be replaced with `b_src.startswith(rb"abc")`
|
||||
9 | if re.match(rb"abc", b_src):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
10 | pass
|
||||
|
|
||||
= help: Replace with `b_src.startswith(rb"abc")`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | re.sub(rb"x", b"y", b_src)
|
||||
7 7 |
|
||||
8 8 | # Should be replaced with `b_src.startswith(rb"abc")`
|
||||
9 |-if re.match(rb"abc", b_src):
|
||||
9 |+if b_src.startswith(rb"abc"):
|
||||
10 10 | pass
|
||||
11 11 |
|
||||
12 12 | # Should be replaced with `rb"x" in b_src`
|
||||
|
||||
RUF055_3.py:13:4: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
12 | # Should be replaced with `rb"x" in b_src`
|
||||
13 | if re.search(rb"x", b_src):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
14 | pass
|
||||
|
|
||||
= help: Replace with `rb"x" in b_src`
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 | pass
|
||||
11 11 |
|
||||
12 12 | # Should be replaced with `rb"x" in b_src`
|
||||
13 |-if re.search(rb"x", b_src):
|
||||
13 |+if rb"x" in b_src:
|
||||
14 14 | pass
|
||||
15 15 |
|
||||
16 16 | # Should be replaced with `b_src.split(rb"abc")`
|
||||
|
||||
RUF055_3.py:17:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
16 | # Should be replaced with `b_src.split(rb"abc")`
|
||||
17 | re.split(rb"abc", b_src)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
18 |
|
||||
19 | # Patterns containing metacharacters should NOT be replaced
|
||||
|
|
||||
= help: Replace with `b_src.split(rb"abc")`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 | pass
|
||||
15 15 |
|
||||
16 16 | # Should be replaced with `b_src.split(rb"abc")`
|
||||
17 |-re.split(rb"abc", b_src)
|
||||
17 |+b_src.split(rb"abc")
|
||||
18 18 |
|
||||
19 19 | # Patterns containing metacharacters should NOT be replaced
|
||||
20 20 | re.sub(rb"ab[c]", b"", b_src)
|
||||
@@ -1527,7 +1527,7 @@ impl<'src> Parser<'src> {
|
||||
self.bump(kind.start_token());
|
||||
let elements = self.parse_interpolated_string_elements(
|
||||
flags,
|
||||
InterpolatedStringElementsKind::Regular,
|
||||
InterpolatedStringElementsKind::Regular(kind),
|
||||
kind,
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use crate::error::UnsupportedSyntaxError;
|
||||
use crate::parser::expression::ExpressionContext;
|
||||
use crate::parser::progress::{ParserProgress, TokenId};
|
||||
use crate::string::InterpolatedStringKind;
|
||||
use crate::token::TokenValue;
|
||||
use crate::token_set::TokenSet;
|
||||
use crate::token_source::{TokenSource, TokenSourceCheckpoint};
|
||||
@@ -799,7 +800,7 @@ impl WithItemKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
enum InterpolatedStringElementsKind {
|
||||
/// The regular f-string elements.
|
||||
///
|
||||
@@ -807,7 +808,7 @@ enum InterpolatedStringElementsKind {
|
||||
/// ```py
|
||||
/// f"hello {x:.2f} world"
|
||||
/// ```
|
||||
Regular,
|
||||
Regular(InterpolatedStringKind),
|
||||
|
||||
/// The f-string elements are part of the format specifier.
|
||||
///
|
||||
@@ -819,15 +820,13 @@ enum InterpolatedStringElementsKind {
|
||||
}
|
||||
|
||||
impl InterpolatedStringElementsKind {
|
||||
const fn list_terminators(self) -> TokenSet {
|
||||
const fn list_terminator(self) -> TokenKind {
|
||||
match self {
|
||||
InterpolatedStringElementsKind::Regular => {
|
||||
TokenSet::new([TokenKind::FStringEnd, TokenKind::TStringEnd])
|
||||
}
|
||||
InterpolatedStringElementsKind::Regular(string_kind) => string_kind.end_token(),
|
||||
// test_ok fstring_format_spec_terminator
|
||||
// f"hello {x:} world"
|
||||
// f"hello {x:.3f} world"
|
||||
InterpolatedStringElementsKind::FormatSpec => TokenSet::new([TokenKind::Rbrace]),
|
||||
InterpolatedStringElementsKind::FormatSpec => TokenKind::Rbrace,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1121,7 +1120,7 @@ impl RecoveryContextKind {
|
||||
.then_some(ListTerminatorKind::Regular),
|
||||
},
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => {
|
||||
if p.at_ts(kind.list_terminators()) {
|
||||
if p.at(kind.list_terminator()) {
|
||||
Some(ListTerminatorKind::Regular)
|
||||
} else {
|
||||
// test_err unterminated_fstring_newline_recovery
|
||||
@@ -1177,13 +1176,23 @@ impl RecoveryContextKind {
|
||||
) || p.at_name_or_soft_keyword()
|
||||
}
|
||||
RecoveryContextKind::WithItems(_) => p.at_expr(),
|
||||
RecoveryContextKind::InterpolatedStringElements(_) => matches!(
|
||||
p.current_token_kind(),
|
||||
// Literal element
|
||||
TokenKind::FStringMiddle | TokenKind::TStringMiddle
|
||||
// Expression element
|
||||
| TokenKind::Lbrace
|
||||
),
|
||||
RecoveryContextKind::InterpolatedStringElements(elements_kind) => {
|
||||
match elements_kind {
|
||||
InterpolatedStringElementsKind::Regular(interpolated_string_kind) => {
|
||||
p.current_token_kind() == interpolated_string_kind.middle_token()
|
||||
|| p.current_token_kind() == TokenKind::Lbrace
|
||||
}
|
||||
InterpolatedStringElementsKind::FormatSpec => {
|
||||
matches!(
|
||||
p.current_token_kind(),
|
||||
// Literal element
|
||||
TokenKind::FStringMiddle | TokenKind::TStringMiddle
|
||||
// Expression element
|
||||
| TokenKind::Lbrace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1272,8 +1281,8 @@ impl RecoveryContextKind {
|
||||
),
|
||||
},
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => match kind {
|
||||
InterpolatedStringElementsKind::Regular => ParseErrorType::OtherError(
|
||||
"Expected an f-string or t-string element or the end of the f-string or t-string".to_string(),
|
||||
InterpolatedStringElementsKind::Regular(string_kind) => ParseErrorType::OtherError(
|
||||
format!("Expected an element of or the end of the {string_kind}"),
|
||||
),
|
||||
InterpolatedStringElementsKind::FormatSpec => ParseErrorType::OtherError(
|
||||
"Expected an f-string or t-string element or a '}'".to_string(),
|
||||
@@ -1316,8 +1325,9 @@ bitflags! {
|
||||
const WITH_ITEMS_PARENTHESIZED = 1 << 25;
|
||||
const WITH_ITEMS_PARENTHESIZED_EXPRESSION = 1 << 26;
|
||||
const WITH_ITEMS_UNPARENTHESIZED = 1 << 28;
|
||||
const FT_STRING_ELEMENTS = 1 << 29;
|
||||
const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30;
|
||||
const F_STRING_ELEMENTS = 1 << 29;
|
||||
const T_STRING_ELEMENTS = 1 << 30;
|
||||
const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 31;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1371,7 +1381,13 @@ impl RecoveryContext {
|
||||
WithItemKind::Unparenthesized => RecoveryContext::WITH_ITEMS_UNPARENTHESIZED,
|
||||
},
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => match kind {
|
||||
InterpolatedStringElementsKind::Regular => RecoveryContext::FT_STRING_ELEMENTS,
|
||||
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::FString) => {
|
||||
RecoveryContext::F_STRING_ELEMENTS
|
||||
}
|
||||
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString) => {
|
||||
RecoveryContext::T_STRING_ELEMENTS
|
||||
}
|
||||
|
||||
InterpolatedStringElementsKind::FormatSpec => {
|
||||
RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC
|
||||
}
|
||||
@@ -1442,8 +1458,11 @@ impl RecoveryContext {
|
||||
RecoveryContext::WITH_ITEMS_UNPARENTHESIZED => {
|
||||
RecoveryContextKind::WithItems(WithItemKind::Unparenthesized)
|
||||
}
|
||||
RecoveryContext::FT_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
|
||||
InterpolatedStringElementsKind::Regular,
|
||||
RecoveryContext::F_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
|
||||
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::FString),
|
||||
),
|
||||
RecoveryContext::T_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
|
||||
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString),
|
||||
),
|
||||
RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC => {
|
||||
RecoveryContextKind::InterpolatedStringElements(
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/src/parser/tests.rs
|
||||
expression: error
|
||||
---
|
||||
ParseError {
|
||||
error: Lexical(
|
||||
LineContinuationError,
|
||||
),
|
||||
location: 3..4,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/src/parser/tests.rs
|
||||
expression: error
|
||||
---
|
||||
ParseError {
|
||||
error: Lexical(
|
||||
TStringError(
|
||||
SingleRbrace,
|
||||
),
|
||||
),
|
||||
location: 8..9,
|
||||
}
|
||||
@@ -134,3 +134,26 @@ foo.bar[0].baz[2].egg??
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(parsed.syntax());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_expr_inner_line_continuation_and_t_string() {
|
||||
let source = r#"f'{\t"i}'"#;
|
||||
|
||||
let parsed = parse_expression(source);
|
||||
|
||||
let error = parsed.unwrap_err();
|
||||
|
||||
insta::assert_debug_snapshot!(error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_expr_inner_line_continuation_newline_t_string() {
|
||||
let source = r#"f'{\
|
||||
t"i}'"#;
|
||||
|
||||
let parsed = parse_expression(source);
|
||||
|
||||
let error = parsed.unwrap_err();
|
||||
|
||||
insta::assert_debug_snapshot!(error);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ impl From<StringType> for Expr {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum InterpolatedStringKind {
|
||||
FString,
|
||||
TString,
|
||||
|
||||
@@ -124,5 +124,5 @@ Module(
|
||||
|
||||
|
|
||||
1 | f"{lambda x: x}"
|
||||
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string
|
||||
| ^ Syntax Error: Expected an element of or the end of the f-string
|
||||
|
|
||||
|
||||
@@ -221,7 +221,7 @@ Module(
|
||||
2 | 'hello'
|
||||
3 | f'world {x}
|
||||
4 | )
|
||||
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string
|
||||
| ^ Syntax Error: Expected an element of or the end of the f-string
|
||||
5 | 1 + 1
|
||||
6 | (
|
||||
|
|
||||
|
||||
@@ -128,5 +128,5 @@ Module(
|
||||
|
|
||||
1 | # parse_options: {"target-version": "3.14"}
|
||||
2 | t"{lambda x: x}"
|
||||
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string
|
||||
| ^ Syntax Error: Expected an element of or the end of the t-string
|
||||
|
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -77,10 +77,31 @@ struct ExpandedEdit {
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
/// Perform global constructor initialization.
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[expect(unsafe_code)]
|
||||
pub fn before_main() {
|
||||
unsafe extern "C" {
|
||||
fn __wasm_call_ctors();
|
||||
}
|
||||
|
||||
// Salsa uses the `inventory` crate, which registers global constructors that may need to be
|
||||
// called explicitly on WASM. See <https://github.com/dtolnay/inventory/blob/master/src/lib.rs#L105>
|
||||
// for details.
|
||||
unsafe {
|
||||
__wasm_call_ctors();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn before_main() {}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run() {
|
||||
use log::Level;
|
||||
|
||||
before_main();
|
||||
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function at least once during initialization, and then
|
||||
// we will get better error messages if our code ever panics.
|
||||
|
||||
@@ -21,6 +21,8 @@ macro_rules! check {
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn empty_config() {
|
||||
ruff_wasm::before_main();
|
||||
|
||||
check!(
|
||||
"if (1, 2):\n pass",
|
||||
r#"{}"#,
|
||||
@@ -42,6 +44,8 @@ fn empty_config() {
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn syntax_error() {
|
||||
ruff_wasm::before_main();
|
||||
|
||||
check!(
|
||||
"x =\ny = 1\n",
|
||||
r#"{}"#,
|
||||
@@ -63,6 +67,8 @@ fn syntax_error() {
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn unsupported_syntax_error() {
|
||||
ruff_wasm::before_main();
|
||||
|
||||
check!(
|
||||
"match 2:\n case 1: ...",
|
||||
r#"{"target-version": "py39"}"#,
|
||||
@@ -84,11 +90,15 @@ fn unsupported_syntax_error() {
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn partial_config() {
|
||||
ruff_wasm::before_main();
|
||||
|
||||
check!("if (1, 2):\n pass", r#"{"ignore": ["F"]}"#, []);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn partial_nested_config() {
|
||||
ruff_wasm::before_main();
|
||||
|
||||
let config = r#"{
|
||||
"select": ["Q"],
|
||||
"flake8-quotes": {
|
||||
|
||||
141
crates/ty/docs/rules.md
generated
141
crates/ty/docs/rules.md
generated
@@ -36,7 +36,7 @@ def test(): -> "int":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L99)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L100)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L143)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -88,7 +88,7 @@ f(int) # error
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L170)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -117,7 +117,7 @@ a = 1
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L194)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L195)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -147,7 +147,7 @@ class C(A, B): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L220)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -177,7 +177,7 @@ class B(A): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L264)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -202,7 +202,7 @@ class B(A, A): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L427)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -334,7 +334,7 @@ class C(A, B): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L451)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L473)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L317)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L339)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -445,7 +445,7 @@ an atypical memory layout.
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L511)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -496,7 +496,7 @@ a: int = ''
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1515)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L555)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -550,7 +550,7 @@ class A(42): ... # error: [invalid-base]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L584)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L606)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -575,7 +575,7 @@ with 1:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L605)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -602,7 +602,7 @@ a: str
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -644,7 +644,7 @@ except ZeroDivisionError:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L664)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L686)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -675,7 +675,7 @@ class C[U](Generic[T]): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L690)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -708,7 +708,7 @@ def f(t: TypeVar("U")): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L761)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -740,7 +740,7 @@ class B(metaclass=f): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -788,7 +788,7 @@ def foo(x: int) -> int: ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L831)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -812,7 +812,7 @@ def f(a: int = ''): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L399)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L421)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -844,7 +844,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L829)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L851)
|
||||
</small>
|
||||
|
||||
Checks for `raise` statements that raise non-exceptions or use invalid
|
||||
@@ -891,7 +891,7 @@ def g():
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L492)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L514)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -914,7 +914,7 @@ def func() -> int:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L872)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -968,7 +968,7 @@ TODO #14889
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L718)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L740)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -993,7 +993,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L933)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1021,7 +1021,7 @@ TYPE_CHECKING = ''
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L935)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1049,7 +1049,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L987)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1009)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1081,7 +1081,7 @@ f(10) # Error
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1113,7 +1113,7 @@ class C:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1015)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1037)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1146,7 +1146,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1044)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1066)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1169,7 +1169,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1196,7 +1196,7 @@ func("string") # error: [no-matching-overload]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1108)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1218,7 +1218,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1104)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1126)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1242,7 +1242,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1155)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1177)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1296,7 +1296,7 @@ def test(): -> "int":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1491)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1513)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1324,7 +1324,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1351,7 +1351,7 @@ class B(A): ... # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1313)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1376,7 +1376,7 @@ f("foo") # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1269)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1402,7 +1402,7 @@ def _(x: int):
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1334)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1446,7 +1446,7 @@ class A:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1369)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1391)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1471,7 +1471,7 @@ f(x=1, y=2) # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1390)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1497,7 +1497,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1434)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1520,7 +1520,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1431)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1543,7 +1543,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1124)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1146)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1578,7 +1578,7 @@ b1 < b2 < b1 # exception raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1450)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1604,7 +1604,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1494)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1622,6 +1622,31 @@ l = list(range(10))
|
||||
l[1:10:0] # ValueError: slice step cannot be zero
|
||||
```
|
||||
|
||||
## `deprecated`
|
||||
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L265)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for uses of deprecated items
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Deprecated items should no longer be used.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
@warnings.deprecated("use new_func instead")
|
||||
def old_func(): ...
|
||||
|
||||
old_func() # emits [deprecated] diagnostic
|
||||
```
|
||||
|
||||
## `invalid-ignore-comment`
|
||||
|
||||
<small>
|
||||
@@ -1655,7 +1680,7 @@ a = 20 / 0 # type: ignore
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1681,7 +1706,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L117)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L118)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1711,7 +1736,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1220)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1741,7 +1766,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1543)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1565)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1766,7 +1791,7 @@ cast(int, f()) # Redundant
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1373)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1817,7 +1842,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1586)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1871,7 +1896,7 @@ def g():
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L551)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L573)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1908,7 +1933,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
<small>
|
||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L246)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L247)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1930,7 +1955,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
<small>
|
||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1224)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
@@ -156,7 +156,12 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
Ok("short") => write!(stdout, "{}", db.salsa_memory_dump().display_short())?,
|
||||
Ok("mypy_primer") => write!(stdout, "{}", db.salsa_memory_dump().display_mypy_primer())?,
|
||||
Ok("full") => write!(stdout, "{}", db.salsa_memory_dump().display_full())?,
|
||||
_ => {}
|
||||
Ok(other) => {
|
||||
tracing::warn!(
|
||||
"Unknown value for `TY_MEMORY_REPORT`: `{other}`. Valid values are `short`, `mypy_primer`, and `full`."
|
||||
);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
std::mem::forget(db);
|
||||
|
||||
@@ -660,7 +660,7 @@ fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
|
||||
--> test.py:4:13
|
||||
|
|
||||
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...
|
||||
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]`
|
||||
|
|
||||
|
||||
@@ -15,7 +15,7 @@ use ty_project::metadata::pyproject::{PyProject, Tool};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
|
||||
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
|
||||
use ty_python_semantic::{ModuleName, PythonPlatform, resolve_module};
|
||||
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module};
|
||||
|
||||
struct TestCase {
|
||||
db: ProjectDatabase,
|
||||
@@ -40,6 +40,14 @@ impl TestCase {
|
||||
&self.db
|
||||
}
|
||||
|
||||
/// Stops file-watching and returns the collected change events.
|
||||
///
|
||||
/// The caller must pass a `MatchEvent` filter that is applied to
|
||||
/// the change events returned. To get all change events, use `|_:
|
||||
/// &ChangeEvent| true`. If possible, callers should pass a filter for a
|
||||
/// specific file name, e.g., `event_for_file("foo.py")`. When done this
|
||||
/// way, the watcher will specifically try to wait for a change event
|
||||
/// matching the filter. This can help avoid flakes.
|
||||
#[track_caller]
|
||||
fn stop_watch<M>(&mut self, matcher: M) -> Vec<ChangeEvent>
|
||||
where
|
||||
@@ -1877,3 +1885,156 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This tests that retrieving submodules from a module has its cache
|
||||
/// appropriately invalidated after a file is created.
|
||||
#[test]
|
||||
fn submodule_cache_invalidation_created() -> anyhow::Result<()> {
|
||||
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
|
||||
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
|
||||
let get_submodules = |db: &dyn Db, module: &Module| {
|
||||
let mut names = module
|
||||
.all_submodules(db)
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names.join("\n")
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
std::fs::write(case.project_path("bar/wazoo.py").as_std_path(), "")?;
|
||||
let changes = case.stop_watch(event_for_file("wazoo.py"));
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This tests that retrieving submodules from a module has its cache
|
||||
/// appropriately invalidated after a file is deleted.
|
||||
#[test]
|
||||
fn submodule_cache_invalidation_deleted() -> anyhow::Result<()> {
|
||||
let mut case = setup([
|
||||
("lib.py", ""),
|
||||
("bar/__init__.py", ""),
|
||||
("bar/foo.py", ""),
|
||||
("bar/wazoo.py", ""),
|
||||
])?;
|
||||
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
|
||||
let get_submodules = |db: &dyn Db, module: &Module| {
|
||||
let mut names = module
|
||||
.all_submodules(db)
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names.join("\n")
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
",
|
||||
);
|
||||
|
||||
std::fs::remove_file(case.project_path("bar/wazoo.py").as_std_path())?;
|
||||
let changes = case.stop_watch(event_for_file("wazoo.py"));
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This tests that retrieving submodules from a module has its cache
|
||||
/// appropriately invalidated after a file is created and then deleted.
|
||||
#[test]
|
||||
fn submodule_cache_invalidation_created_then_deleted() -> anyhow::Result<()> {
|
||||
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
|
||||
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
|
||||
let get_submodules = |db: &dyn Db, module: &Module| {
|
||||
let mut names = module
|
||||
.all_submodules(db)
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names.join("\n")
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
std::fs::write(case.project_path("bar/wazoo.py").as_std_path(), "")?;
|
||||
let changes = case.take_watch_changes(event_for_file("wazoo.py"));
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
std::fs::remove_file(case.project_path("bar/wazoo.py").as_std_path())?;
|
||||
let changes = case.stop_watch(event_for_file("wazoo.py"));
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This tests that retrieving submodules from a module has its cache
|
||||
/// appropriately invalidated after a file is created *after* a project
|
||||
/// configuration change.
|
||||
#[test]
|
||||
fn submodule_cache_invalidation_after_pyproject_created() -> anyhow::Result<()> {
|
||||
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
|
||||
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
|
||||
let get_submodules = |db: &dyn Db, module: &Module| {
|
||||
let mut names = module
|
||||
.all_submodules(db)
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names.join("\n")
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
case.update_options(Options::default())?;
|
||||
|
||||
std::fs::write(case.project_path("bar/wazoo.py").as_std_path(), "")?;
|
||||
let changes = case.take_watch_changes(event_for_file("wazoo.py"));
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1618,6 +1618,8 @@ Answer.<CURSOR>
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
__weakrefoffset__ :: int
|
||||
_add_alias_ :: def _add_alias_(self, name: str) -> None
|
||||
_add_value_alias_ :: def _add_value_alias_(self, value: Any) -> None
|
||||
_generate_next_value_ :: def _generate_next_value_(name: str, start: int, count: int, last_values: list[Any]) -> Any
|
||||
_ignore_ :: str | list[str]
|
||||
_member_map_ :: dict[str, Enum]
|
||||
|
||||
@@ -96,7 +96,7 @@ pub(crate) mod tests {
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for TestDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
fn should_check_file(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,12 @@ impl<'a> CoveringNode<'a> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the ancestor nodes, starting from the root
|
||||
/// and ending with the covering node.
|
||||
pub(crate) fn ancestors(&self) -> impl Iterator<Item = AnyNodeRef<'a>> + '_ {
|
||||
self.nodes.iter().copied()
|
||||
}
|
||||
|
||||
/// Finds the index of the node that fully covers the range and
|
||||
/// fulfills the given predicate.
|
||||
///
|
||||
|
||||
@@ -9,22 +9,53 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use ty_python_semantic::types::Type;
|
||||
use ty_python_semantic::{HasType, SemanticModel};
|
||||
use ty_python_semantic::types::definitions_for_keyword_argument;
|
||||
use ty_python_semantic::{
|
||||
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum GotoTarget<'a> {
|
||||
Expression(ast::ExprRef<'a>),
|
||||
FunctionDef(&'a ast::StmtFunctionDef),
|
||||
ClassDef(&'a ast::StmtClassDef),
|
||||
Parameter(&'a ast::Parameter),
|
||||
Alias(&'a ast::Alias),
|
||||
|
||||
/// Go to on the module name of an import from
|
||||
/// Multi-part module names
|
||||
/// Handles both `import foo.bar` and `from foo.bar import baz` cases
|
||||
/// ```py
|
||||
/// from foo import bar
|
||||
/// ^^^
|
||||
/// import foo.bar
|
||||
/// ^^^
|
||||
/// from foo.bar import baz
|
||||
/// ^^^
|
||||
/// ```
|
||||
ImportedModule(&'a ast::StmtImportFrom),
|
||||
ImportModuleComponent {
|
||||
module_name: String,
|
||||
component_index: usize,
|
||||
component_range: TextRange,
|
||||
},
|
||||
|
||||
/// Import alias in standard import statement
|
||||
/// ```py
|
||||
/// import foo.bar as baz
|
||||
/// ^^^
|
||||
/// ```
|
||||
ImportModuleAlias {
|
||||
alias: &'a ast::Alias,
|
||||
},
|
||||
|
||||
/// Import alias in from import statement
|
||||
/// ```py
|
||||
/// from foo import bar as baz
|
||||
/// ^^^
|
||||
/// from foo import bar as baz
|
||||
/// ^^^
|
||||
/// ```
|
||||
ImportSymbolAlias {
|
||||
alias: &'a ast::Alias,
|
||||
range: TextRange,
|
||||
import_from: &'a ast::StmtImportFrom,
|
||||
},
|
||||
|
||||
/// Go to on the exception handler variable
|
||||
/// ```py
|
||||
@@ -39,7 +70,10 @@ pub(crate) enum GotoTarget<'a> {
|
||||
/// test(a = 1)
|
||||
/// ^
|
||||
/// ```
|
||||
KeywordArgument(&'a ast::Keyword),
|
||||
KeywordArgument {
|
||||
keyword: &'a ast::Keyword,
|
||||
call_expression: &'a ast::ExprCall,
|
||||
},
|
||||
|
||||
/// Go to on the rest parameter of a pattern match
|
||||
///
|
||||
@@ -108,25 +142,22 @@ pub(crate) enum GotoTarget<'a> {
|
||||
}
|
||||
|
||||
impl GotoTarget<'_> {
|
||||
pub(crate) fn inferred_type<'db>(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
|
||||
pub(crate) fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
|
||||
let ty = match self {
|
||||
GotoTarget::Expression(expression) => expression.inferred_type(model),
|
||||
GotoTarget::FunctionDef(function) => function.inferred_type(model),
|
||||
GotoTarget::ClassDef(class) => class.inferred_type(model),
|
||||
GotoTarget::Parameter(parameter) => parameter.inferred_type(model),
|
||||
GotoTarget::Alias(alias) => alias.inferred_type(model),
|
||||
GotoTarget::ImportSymbolAlias { alias, .. } => alias.inferred_type(model),
|
||||
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
|
||||
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
|
||||
GotoTarget::KeywordArgument(argument) => {
|
||||
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
|
||||
// than using the inferred value.
|
||||
argument.value.inferred_type(model)
|
||||
}
|
||||
GotoTarget::KeywordArgument { keyword, .. } => keyword.value.inferred_type(model),
|
||||
// TODO: Support identifier targets
|
||||
GotoTarget::PatternMatchRest(_)
|
||||
| GotoTarget::PatternKeywordArgument(_)
|
||||
| GotoTarget::PatternMatchStarName(_)
|
||||
| GotoTarget::PatternMatchAsName(_)
|
||||
| GotoTarget::ImportedModule(_)
|
||||
| GotoTarget::ImportModuleComponent { .. }
|
||||
| GotoTarget::TypeParamTypeVarName(_)
|
||||
| GotoTarget::TypeParamParamSpecName(_)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(_)
|
||||
@@ -141,7 +172,7 @@ impl GotoTarget<'_> {
|
||||
/// If a stub mapper is provided, definitions from stub files will be mapped to
|
||||
/// their corresponding source file implementations.
|
||||
pub(crate) fn get_definition_targets(
|
||||
self,
|
||||
&self,
|
||||
file: ruff_db::files::File,
|
||||
db: &dyn crate::Db,
|
||||
stub_mapper: Option<&StubMapper>,
|
||||
@@ -150,15 +181,19 @@ impl GotoTarget<'_> {
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
match self {
|
||||
// For names, find the definitions of the symbol
|
||||
GotoTarget::Expression(expression) => {
|
||||
if let ast::ExprRef::Name(name) = expression {
|
||||
Self::get_name_definition_targets(name, file, db, stub_mapper)
|
||||
} else {
|
||||
// For other expressions, we can't find definitions
|
||||
None
|
||||
}
|
||||
}
|
||||
GotoTarget::Expression(expression) => match expression {
|
||||
ast::ExprRef::Name(name) => definitions_to_navigation_targets(
|
||||
db,
|
||||
stub_mapper,
|
||||
definitions_for_name(db, file, name),
|
||||
),
|
||||
ast::ExprRef::Attribute(attribute) => definitions_to_navigation_targets(
|
||||
db,
|
||||
stub_mapper,
|
||||
ty_python_semantic::definitions_for_attribute(db, file, attribute),
|
||||
),
|
||||
_ => None,
|
||||
},
|
||||
|
||||
// For already-defined symbols, they are their own definitions
|
||||
GotoTarget::FunctionDef(function) => {
|
||||
@@ -188,48 +223,57 @@ impl GotoTarget<'_> {
|
||||
}))
|
||||
}
|
||||
|
||||
// For imports, find the symbol being imported
|
||||
GotoTarget::Alias(_alias) => {
|
||||
// For aliases, we don't have the ExprName node, so we can't get the scope
|
||||
// For now, return None. In the future, we could look up the imported symbol
|
||||
None
|
||||
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
|
||||
GotoTarget::ImportSymbolAlias {
|
||||
alias, import_from, ..
|
||||
} => {
|
||||
// Handle both original names and alias names in `from x import y as z` statements
|
||||
let symbol_name = alias.name.as_str();
|
||||
let definitions =
|
||||
definitions_for_imported_symbol(db, file, import_from, symbol_name);
|
||||
|
||||
definitions_to_navigation_targets(db, stub_mapper, definitions)
|
||||
}
|
||||
|
||||
GotoTarget::ImportModuleComponent {
|
||||
module_name,
|
||||
component_index,
|
||||
..
|
||||
} => {
|
||||
// Handle both `import foo.bar` and `from foo.bar import baz` where offset is within module component
|
||||
let components: Vec<&str> = module_name.split('.').collect();
|
||||
|
||||
// Build the module name up to and including the component containing the offset
|
||||
let target_module_name = components[..=*component_index].join(".");
|
||||
|
||||
// Try to resolve the module
|
||||
resolve_module_to_navigation_target(db, &target_module_name)
|
||||
}
|
||||
|
||||
// Handle import aliases (offset within 'z' in "import x.y as z")
|
||||
GotoTarget::ImportModuleAlias { alias } => {
|
||||
// For import aliases, navigate to the module being aliased
|
||||
// This only applies to regular import statements like "import x.y as z"
|
||||
let full_module_name = alias.name.as_str();
|
||||
|
||||
// Try to resolve the module
|
||||
resolve_module_to_navigation_target(db, full_module_name)
|
||||
}
|
||||
|
||||
// Handle keyword arguments in call expressions
|
||||
GotoTarget::KeywordArgument {
|
||||
keyword,
|
||||
call_expression,
|
||||
} => {
|
||||
let definitions =
|
||||
definitions_for_keyword_argument(db, file, keyword, call_expression);
|
||||
definitions_to_navigation_targets(db, stub_mapper, definitions)
|
||||
}
|
||||
|
||||
// TODO: Handle attribute and method accesses (y in `x.y` expressions)
|
||||
// TODO: Handle keyword arguments in call expression
|
||||
// TODO: Handle multi-part module names in import statements
|
||||
// TODO: Handle imported symbol in y in `from x import y as z` statement
|
||||
// TODO: Handle string literals that map to TypedDict fields
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get navigation targets for definitions associated with a name expression
|
||||
fn get_name_definition_targets(
|
||||
name: &ruff_python_ast::ExprName,
|
||||
file: ruff_db::files::File,
|
||||
db: &dyn crate::Db,
|
||||
stub_mapper: Option<&StubMapper>,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
use ty_python_semantic::definitions_for_name;
|
||||
|
||||
// Get all definitions for this name
|
||||
let mut definitions = definitions_for_name(db, file, name);
|
||||
|
||||
// Apply stub mapping if a mapper is provided
|
||||
if let Some(mapper) = stub_mapper {
|
||||
definitions = mapper.map_definitions(definitions);
|
||||
}
|
||||
|
||||
if definitions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Convert definitions to navigation targets
|
||||
let targets = convert_resolved_definitions_to_targets(db, definitions);
|
||||
|
||||
Some(crate::NavigationTargets::unique(targets))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for GotoTarget<'_> {
|
||||
@@ -239,10 +283,13 @@ impl Ranged for GotoTarget<'_> {
|
||||
GotoTarget::FunctionDef(function) => function.name.range,
|
||||
GotoTarget::ClassDef(class) => class.name.range,
|
||||
GotoTarget::Parameter(parameter) => parameter.name.range,
|
||||
GotoTarget::Alias(alias) => alias.name.range,
|
||||
GotoTarget::ImportedModule(module) => module.module.as_ref().unwrap().range,
|
||||
GotoTarget::ImportSymbolAlias { range, .. } => *range,
|
||||
GotoTarget::ImportModuleComponent {
|
||||
component_range, ..
|
||||
} => *component_range,
|
||||
GotoTarget::ImportModuleAlias { alias } => alias.asname.as_ref().unwrap().range,
|
||||
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
|
||||
GotoTarget::KeywordArgument(keyword) => keyword.arg.as_ref().unwrap().range,
|
||||
GotoTarget::KeywordArgument { keyword, .. } => keyword.arg.as_ref().unwrap().range,
|
||||
GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range,
|
||||
GotoTarget::PatternKeywordArgument(keyword) => keyword.attr.range,
|
||||
GotoTarget::PatternMatchStarName(star) => star.name.as_ref().unwrap().range,
|
||||
@@ -279,18 +326,35 @@ fn convert_resolved_definitions_to_targets(
|
||||
full_range: full_range.range(),
|
||||
}
|
||||
}
|
||||
ty_python_semantic::ResolvedDefinition::ModuleFile(module_file) => {
|
||||
// For module files, navigate to the beginning of the file
|
||||
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
|
||||
// For file ranges, navigate to the specific range within the file
|
||||
crate::NavigationTarget {
|
||||
file: module_file,
|
||||
focus_range: ruff_text_size::TextRange::default(), // Start of file
|
||||
full_range: ruff_text_size::TextRange::default(), // Start of file
|
||||
file: file_range.file(),
|
||||
focus_range: file_range.range(),
|
||||
full_range: file_range.range(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Shared helper to map and convert resolved definitions into navigation targets.
|
||||
fn definitions_to_navigation_targets<'db>(
|
||||
db: &dyn crate::Db,
|
||||
stub_mapper: Option<&StubMapper<'db>>,
|
||||
mut definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
if let Some(mapper) = stub_mapper {
|
||||
definitions = mapper.map_definitions(definitions);
|
||||
}
|
||||
if definitions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let targets = convert_resolved_definitions_to_targets(db, definitions);
|
||||
Some(crate::NavigationTargets::unique(targets))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target(
|
||||
parsed: &ParsedModuleRef,
|
||||
offset: TextSize,
|
||||
@@ -318,12 +382,102 @@ pub(crate) fn find_goto_target(
|
||||
Some(AnyNodeRef::StmtFunctionDef(function)) => Some(GotoTarget::FunctionDef(function)),
|
||||
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
|
||||
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
|
||||
Some(AnyNodeRef::Alias(alias)) => Some(GotoTarget::Alias(alias)),
|
||||
Some(AnyNodeRef::StmtImportFrom(from)) => Some(GotoTarget::ImportedModule(from)),
|
||||
Some(AnyNodeRef::Alias(alias)) => {
|
||||
// Find the containing import statement to determine the type
|
||||
let import_stmt = covering_node.ancestors().find(|node| {
|
||||
matches!(
|
||||
node,
|
||||
AnyNodeRef::StmtImport(_) | AnyNodeRef::StmtImportFrom(_)
|
||||
)
|
||||
});
|
||||
|
||||
match import_stmt {
|
||||
Some(AnyNodeRef::StmtImport(_)) => {
|
||||
// Regular import statement like "import x.y as z"
|
||||
|
||||
// Is the offset within the alias name (asname) part?
|
||||
if let Some(asname) = &alias.asname {
|
||||
if asname.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportModuleAlias { alias });
|
||||
}
|
||||
}
|
||||
|
||||
// Is the offset in the module name part?
|
||||
if alias.name.range.contains_inclusive(offset) {
|
||||
let full_name = alias.name.as_str();
|
||||
|
||||
if let Some((component_index, component_range)) =
|
||||
find_module_component(full_name, alias.name.range.start(), offset)
|
||||
{
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_name.to_string(),
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Some(AnyNodeRef::StmtImportFrom(import_from)) => {
|
||||
// From import statement like "from x import y as z"
|
||||
|
||||
// Is the offset within the alias name (asname) part?
|
||||
if let Some(asname) = &alias.asname {
|
||||
if asname.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportSymbolAlias {
|
||||
alias,
|
||||
range: asname.range,
|
||||
import_from,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Is the offset in the original name part?
|
||||
if alias.name.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportSymbolAlias {
|
||||
alias,
|
||||
range: alias.name.range,
|
||||
import_from,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Some(AnyNodeRef::StmtImportFrom(from)) => {
|
||||
// Handle offset within module name in from import statements
|
||||
if let Some(module_expr) = &from.module {
|
||||
let full_module_name = module_expr.to_string();
|
||||
|
||||
if let Some((component_index, component_range)) =
|
||||
find_module_component(&full_module_name, module_expr.range.start(), offset)
|
||||
{
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_module_name,
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
|
||||
Some(GotoTarget::ExceptVariable(handler))
|
||||
}
|
||||
Some(AnyNodeRef::Keyword(keyword)) => Some(GotoTarget::KeywordArgument(keyword)),
|
||||
Some(AnyNodeRef::Keyword(keyword)) => {
|
||||
// Find the containing call expression from the ancestor chain
|
||||
let call_expression = covering_node
|
||||
.ancestors()
|
||||
.find_map(ruff_python_ast::AnyNodeRef::expr_call)?;
|
||||
Some(GotoTarget::KeywordArgument {
|
||||
keyword,
|
||||
call_expression,
|
||||
})
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
|
||||
Some(GotoTarget::PatternMatchRest(mapping))
|
||||
}
|
||||
@@ -361,3 +515,57 @@ pub(crate) fn find_goto_target(
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to resolve a module name and create a navigation target.
|
||||
fn resolve_module_to_navigation_target(
|
||||
db: &dyn crate::Db,
|
||||
module_name_str: &str,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
use ty_python_semantic::{ModuleName, resolve_module};
|
||||
|
||||
if let Some(module_name) = ModuleName::new(module_name_str) {
|
||||
if let Some(resolved_module) = resolve_module(db, &module_name) {
|
||||
if let Some(module_file) = resolved_module.file() {
|
||||
return Some(crate::NavigationTargets::single(crate::NavigationTarget {
|
||||
file: module_file,
|
||||
focus_range: TextRange::default(),
|
||||
full_range: TextRange::default(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Helper function to extract module component information from a dotted module name
|
||||
fn find_module_component(
|
||||
full_module_name: &str,
|
||||
module_start: TextSize,
|
||||
offset: TextSize,
|
||||
) -> Option<(usize, TextRange)> {
|
||||
let pos_in_module = offset - module_start;
|
||||
let pos_in_module = pos_in_module.to_usize();
|
||||
|
||||
// Split the module name into components and find which one contains the offset
|
||||
let mut current_pos = 0;
|
||||
let components: Vec<&str> = full_module_name.split('.').collect();
|
||||
|
||||
for (i, component) in components.iter().enumerate() {
|
||||
let component_start = current_pos;
|
||||
let component_end = current_pos + component.len();
|
||||
|
||||
// Check if the offset is within this component or at its right boundary
|
||||
if pos_in_module >= component_start && pos_in_module <= component_end {
|
||||
let component_range = TextRange::new(
|
||||
module_start + TextSize::from(u32::try_from(component_start).ok()?),
|
||||
module_start + TextSize::from(u32::try_from(component_end).ok()?),
|
||||
);
|
||||
return Some((i, component_range));
|
||||
}
|
||||
|
||||
// Move past this component and the dot
|
||||
current_pos = component_end + 1; // +1 for the dot
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -611,7 +611,330 @@ def another_helper():
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_builtin_type() {
|
||||
fn goto_declaration_import_as_alias_name() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
import mymodule.submodule as su<CURSOR>b
|
||||
print(sub.helper())
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule/__init__.py",
|
||||
"
|
||||
# Main module init
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule/submodule.py",
|
||||
r#"
|
||||
FOO = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mymodule/submodule.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | FOO = 0
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:30
|
||||
|
|
||||
2 | import mymodule.submodule as sub
|
||||
| ^^^
|
||||
3 | print(sub.helper())
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_import_as_alias_name_on_module() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
import mymodule.submod<CURSOR>ule as sub
|
||||
print(sub.helper())
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule/__init__.py",
|
||||
"
|
||||
# Main module init
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule/submodule.py",
|
||||
r#"
|
||||
FOO = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mymodule/submodule.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | FOO = 0
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | import mymodule.submodule as sub
|
||||
| ^^^^^^^^^
|
||||
3 | print(sub.helper())
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_from_import_symbol_original() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from mypackage.utils import hel<CURSOR>per as h
|
||||
result = h("/a", "/b")
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
# Package init
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/utils.py",
|
||||
r#"
|
||||
def helper(a, b):
|
||||
return a + "/" + b
|
||||
|
||||
def another_helper(path):
|
||||
return "processed"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/utils.py:2:5
|
||||
|
|
||||
2 | def helper(a, b):
|
||||
| ^^^^^^
|
||||
3 | return a + "/" + b
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:29
|
||||
|
|
||||
2 | from mypackage.utils import helper as h
|
||||
| ^^^^^^
|
||||
3 | result = h("/a", "/b")
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_from_import_symbol_alias() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from mypackage.utils import helper as h<CURSOR>
|
||||
result = h("/a", "/b")
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
# Package init
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/utils.py",
|
||||
r#"
|
||||
def helper(a, b):
|
||||
return a + "/" + b
|
||||
|
||||
def another_helper(path):
|
||||
return "processed"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/utils.py:2:5
|
||||
|
|
||||
2 | def helper(a, b):
|
||||
| ^^^^^^
|
||||
3 | return a + "/" + b
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:39
|
||||
|
|
||||
2 | from mypackage.utils import helper as h
|
||||
| ^
|
||||
3 | result = h("/a", "/b")
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_from_import_module() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from mypackage.ut<CURSOR>ils import helper as h
|
||||
result = h("/a", "/b")
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
# Package init
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/utils.py",
|
||||
r#"
|
||||
def helper(a, b):
|
||||
return a + "/" + b
|
||||
|
||||
def another_helper(path):
|
||||
return "processed"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/utils.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | def helper(a, b):
|
||||
3 | return a + "/" + b
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:16
|
||||
|
|
||||
2 | from mypackage.utils import helper as h
|
||||
| ^^^^^
|
||||
3 | result = h("/a", "/b")
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_instance_attribute() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
|
||||
c = C()
|
||||
y = c.x<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:21
|
||||
|
|
||||
2 | class C:
|
||||
3 | def __init__(self):
|
||||
4 | self.x: int = 1
|
||||
| ^^^^^^
|
||||
5 |
|
||||
6 | c = C()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:17
|
||||
|
|
||||
6 | c = C()
|
||||
7 | y = c.x
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_instance_attribute_no_annotation() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
c = C()
|
||||
y = c.x<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:21
|
||||
|
|
||||
2 | class C:
|
||||
3 | def __init__(self):
|
||||
4 | self.x = 1
|
||||
| ^^^^^^
|
||||
5 |
|
||||
6 | c = C()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:17
|
||||
|
|
||||
6 | c = C()
|
||||
7 | y = c.x
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_method_call_to_definition() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class C:
|
||||
def foo(self):
|
||||
return 42
|
||||
|
||||
c = C()
|
||||
res = c.foo<CURSOR>()
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:21
|
||||
|
|
||||
2 | class C:
|
||||
3 | def foo(self):
|
||||
| ^^^
|
||||
4 | return 42
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:19
|
||||
|
|
||||
6 | c = C()
|
||||
7 | res = c.foo()
|
||||
| ^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_module_attribute() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
x: i<CURSOR>nt = 42
|
||||
@@ -721,6 +1044,152 @@ def function():
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_inherited_attribute() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class A:
|
||||
x = 10
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
b = B()
|
||||
y = b.x<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:17
|
||||
|
|
||||
2 | class A:
|
||||
3 | x = 10
|
||||
| ^
|
||||
4 |
|
||||
5 | class B(A):
|
||||
|
|
||||
info: Source
|
||||
--> main.py:9:17
|
||||
|
|
||||
8 | b = B()
|
||||
9 | y = b.x
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_property_getter_setter() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class C:
|
||||
def __init__(self):
|
||||
self._value = 0
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
c = C()
|
||||
c.value<CURSOR> = 42
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:7:21
|
||||
|
|
||||
6 | @property
|
||||
7 | def value(self):
|
||||
| ^^^^^
|
||||
8 | return self._value
|
||||
|
|
||||
info: Source
|
||||
--> main.py:11:13
|
||||
|
|
||||
10 | c = C()
|
||||
11 | c.value = 42
|
||||
| ^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_function_doc_attribute() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_function():
|
||||
"""This is a docstring."""
|
||||
return 42
|
||||
|
||||
doc = my_function.__doc<CURSOR>__
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should navigate to the __doc__ property in the FunctionType class in typeshed
|
||||
let result = test.goto_declaration();
|
||||
|
||||
assert!(
|
||||
!result.contains("No goto target found"),
|
||||
"Should find builtin __doc__ attribute"
|
||||
);
|
||||
assert!(
|
||||
!result.contains("No declarations found"),
|
||||
"Should find builtin __doc__ declarations"
|
||||
);
|
||||
|
||||
// Should navigate to a typeshed file containing the __doc__ attribute
|
||||
assert!(
|
||||
result.contains("types.pyi") || result.contains("builtins.pyi"),
|
||||
"Should navigate to typeshed file with __doc__ definition"
|
||||
);
|
||||
assert!(
|
||||
result.contains("__doc__"),
|
||||
"Should find the __doc__ attribute definition"
|
||||
);
|
||||
assert!(
|
||||
result.contains("info[goto-declaration]: Declaration"),
|
||||
"Should be a goto-declaration result"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_protocol_instance_attribute() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
from typing import Protocol
|
||||
|
||||
class Drawable(Protocol):
|
||||
def draw(self) -> None: ...
|
||||
name: str
|
||||
|
||||
def use_drawable(obj: Drawable):
|
||||
obj.na<CURSOR>me
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:6:17
|
||||
|
|
||||
4 | class Drawable(Protocol):
|
||||
5 | def draw(self) -> None: ...
|
||||
6 | name: str
|
||||
| ^^^^
|
||||
7 |
|
||||
8 | def use_drawable(obj: Drawable):
|
||||
|
|
||||
info: Source
|
||||
--> main.py:9:17
|
||||
|
|
||||
8 | def use_drawable(obj: Drawable):
|
||||
9 | obj.name
|
||||
| ^^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_generic_method_class_type() {
|
||||
let test = cursor_test(
|
||||
@@ -756,6 +1225,94 @@ class MyClass:
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_keyword_argument_simple() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def my_function(x, y, z=10):
|
||||
return x + y + z
|
||||
|
||||
result = my_function(1, y<CURSOR>=2, z=3)
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:32
|
||||
|
|
||||
2 | def my_function(x, y, z=10):
|
||||
| ^
|
||||
3 | return x + y + z
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:37
|
||||
|
|
||||
3 | return x + y + z
|
||||
4 |
|
||||
5 | result = my_function(1, y=2, z=3)
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_keyword_argument_overloaded() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def process(data: str, format: str) -> str: ...
|
||||
|
||||
@overload
|
||||
def process(data: int, format: int) -> int: ...
|
||||
|
||||
def process(data, format):
|
||||
return data
|
||||
|
||||
# Call the overloaded function
|
||||
result = process("hello", format<CURSOR>="json")
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should navigate to the parameter in both matching overloads
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:5:36
|
||||
|
|
||||
4 | @overload
|
||||
5 | def process(data: str, format: str) -> str: ...
|
||||
| ^^^^^^
|
||||
6 |
|
||||
7 | @overload
|
||||
|
|
||||
info: Source
|
||||
--> main.py:14:39
|
||||
|
|
||||
13 | # Call the overloaded function
|
||||
14 | result = process("hello", format="json")
|
||||
| ^^^^^^
|
||||
|
|
||||
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:8:36
|
||||
|
|
||||
7 | @overload
|
||||
8 | def process(data: int, format: int) -> int: ...
|
||||
| ^^^^^^
|
||||
9 |
|
||||
10 | def process(data, format):
|
||||
|
|
||||
info: Source
|
||||
--> main.py:14:39
|
||||
|
|
||||
13 | # Call the overloaded function
|
||||
14 | result = process("hello", format="json")
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_declaration(&self) -> String {
|
||||
let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset)
|
||||
|
||||
@@ -198,14 +198,14 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
@@ -227,14 +227,14 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:22
|
||||
@@ -343,14 +343,14 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
@@ -378,14 +378,14 @@ mod tests {
|
||||
// is an int. Navigating to `str` would match pyright's behavior.
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:338:7
|
||||
--> stdlib/builtins.pyi:337:7
|
||||
|
|
||||
336 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
337 |
|
||||
338 | class int:
|
||||
335 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
336 |
|
||||
337 | class int:
|
||||
| ^^^
|
||||
339 | """int([x]) -> integer
|
||||
340 | int(x, base=10) -> integer
|
||||
338 | """int([x]) -> integer
|
||||
339 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
@@ -412,14 +412,14 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:2892:7
|
||||
--> stdlib/builtins.pyi:2888:7
|
||||
|
|
||||
2890 | """See PEP 585"""
|
||||
2891 |
|
||||
2892 | class dict(MutableMapping[_KT, _VT]):
|
||||
2886 | """See PEP 585"""
|
||||
2887 |
|
||||
2888 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
2893 | """dict() -> new empty dictionary
|
||||
2894 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
2889 | """dict() -> new empty dictionary
|
||||
2890 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:5
|
||||
@@ -443,14 +443,14 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
@@ -536,14 +536,14 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:27
|
||||
@@ -567,13 +567,13 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/types.pyi:922:11
|
||||
--> stdlib/types.pyi:921:11
|
||||
|
|
||||
920 | if sys.version_info >= (3, 10):
|
||||
921 | @final
|
||||
922 | class NoneType:
|
||||
919 | if sys.version_info >= (3, 10):
|
||||
920 | @final
|
||||
921 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
923 | """The type of the None singleton."""
|
||||
922 | """The type of the None singleton."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
@@ -584,14 +584,14 @@ f(**kwargs<CURSOR>)
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
|
||||
@@ -105,11 +105,11 @@ pub struct NavigationTargets(smallvec::SmallVec<[NavigationTarget; 1]>);
|
||||
|
||||
impl NavigationTargets {
|
||||
fn single(target: NavigationTarget) -> Self {
|
||||
Self(smallvec::smallvec![target])
|
||||
Self(smallvec::smallvec_inline![target])
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self(smallvec::SmallVec::new())
|
||||
Self(smallvec::SmallVec::new_const())
|
||||
}
|
||||
|
||||
fn unique(targets: impl IntoIterator<Item = NavigationTarget>) -> Self {
|
||||
|
||||
@@ -482,25 +482,40 @@ impl<'db> SemanticTokenVisitor<'db> {
|
||||
parameters: &ast::Parameters,
|
||||
func: Option<&ast::StmtFunctionDef>,
|
||||
) {
|
||||
// Parameters
|
||||
for (i, param) in parameters.args.iter().enumerate() {
|
||||
let token_type = if let Some(func) = func {
|
||||
// For function definitions, use the classification logic to determine
|
||||
// whether this is a self/cls parameter or just a regular parameter
|
||||
self.classify_parameter(¶m.parameter, i == 0, func)
|
||||
} else {
|
||||
// For lambdas, all parameters are just parameters (no self/cls)
|
||||
SemanticTokenType::Parameter
|
||||
let mut param_index = 0;
|
||||
|
||||
for any_param in parameters {
|
||||
let parameter = any_param.as_parameter();
|
||||
|
||||
let token_type = match any_param {
|
||||
ast::AnyParameterRef::NonVariadic(_) => {
|
||||
// For non-variadic parameters (positional-only, regular, keyword-only),
|
||||
// check if this should be classified as self/cls parameter
|
||||
if let Some(func) = func {
|
||||
let result = self.classify_parameter(parameter, param_index == 0, func);
|
||||
param_index += 1;
|
||||
result
|
||||
} else {
|
||||
// For lambdas, all parameters are just parameters (no self/cls)
|
||||
param_index += 1;
|
||||
SemanticTokenType::Parameter
|
||||
}
|
||||
}
|
||||
ast::AnyParameterRef::Variadic(_) => {
|
||||
// Variadic parameters (*args, **kwargs) are always just parameters
|
||||
param_index += 1;
|
||||
SemanticTokenType::Parameter
|
||||
}
|
||||
};
|
||||
|
||||
self.add_token(
|
||||
param.parameter.name.range(),
|
||||
parameter.name.range(),
|
||||
token_type,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
|
||||
// Handle parameter type annotations
|
||||
if let Some(annotation) = ¶m.parameter.annotation {
|
||||
if let Some(annotation) = ¶meter.annotation {
|
||||
self.visit_type_annotation(annotation);
|
||||
}
|
||||
}
|
||||
@@ -977,7 +992,8 @@ class MyClass:
|
||||
class MyClass:
|
||||
def method(instance, x): pass
|
||||
@classmethod
|
||||
def other(klass, y): pass<CURSOR>
|
||||
def other(klass, y): pass
|
||||
def complex_method(instance, posonly, /, regular, *args, kwonly, **kwargs): pass<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
@@ -992,6 +1008,13 @@ class MyClass:
|
||||
"other" @ 75..80: Method [definition]
|
||||
"klass" @ 81..86: ClsParameter
|
||||
"y" @ 88..89: Parameter
|
||||
"complex_method" @ 105..119: Method [definition]
|
||||
"instance" @ 120..128: SelfParameter
|
||||
"posonly" @ 130..137: Parameter
|
||||
"regular" @ 142..149: Parameter
|
||||
"args" @ 152..156: Parameter
|
||||
"kwonly" @ 158..164: Parameter
|
||||
"kwargs" @ 168..174: Parameter
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -1665,6 +1688,12 @@ class BoundedContainer[T: int, U = str]:
|
||||
"P" @ 324..325: Variable
|
||||
"str" @ 327..330: Class
|
||||
"wrapper" @ 341..348: Function [definition]
|
||||
"args" @ 350..354: Parameter
|
||||
"P" @ 356..357: Variable
|
||||
"args" @ 358..362: Variable
|
||||
"kwargs" @ 366..372: Parameter
|
||||
"P" @ 374..375: Variable
|
||||
"kwargs" @ 376..382: Variable
|
||||
"str" @ 387..390: Class
|
||||
"str" @ 407..410: Class
|
||||
"func" @ 411..415: Variable
|
||||
|
||||
@@ -12,8 +12,8 @@ use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use salsa::Event;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Event, Setter};
|
||||
use ty_ide::Db as IdeDb;
|
||||
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use ty_python_semantic::{Db as SemanticDb, Program};
|
||||
@@ -82,22 +82,25 @@ impl ProjectDatabase {
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
/// Checks the files in the project and its dependencies as per the project's check mode.
|
||||
///
|
||||
/// Use [`set_check_mode`] to update the check mode.
|
||||
///
|
||||
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
|
||||
pub fn check(&self) -> Vec<Diagnostic> {
|
||||
self.check_with_mode(CheckMode::OpenFiles)
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies, using the given reporter.
|
||||
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
|
||||
let reporter = AssertUnwindSafe(reporter);
|
||||
self.project().check(self, CheckMode::OpenFiles, reporter)
|
||||
}
|
||||
|
||||
/// Check the project with the given mode.
|
||||
pub fn check_with_mode(&self, mode: CheckMode) -> Vec<Diagnostic> {
|
||||
let mut reporter = DummyReporter;
|
||||
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter);
|
||||
self.project().check(self, mode, reporter)
|
||||
self.project().check(self, reporter)
|
||||
}
|
||||
|
||||
/// Checks the files in the project and its dependencies, using the given reporter.
|
||||
///
|
||||
/// Use [`set_check_mode`] to update the check mode.
|
||||
///
|
||||
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
|
||||
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
|
||||
let reporter = AssertUnwindSafe(reporter);
|
||||
self.project().check(self, reporter)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
@@ -105,6 +108,12 @@ impl ProjectDatabase {
|
||||
self.project().check_file(self, file)
|
||||
}
|
||||
|
||||
/// Set the check mode for the project.
|
||||
pub fn set_check_mode(&mut self, mode: CheckMode) {
|
||||
tracing::debug!("Updating project to check {mode}");
|
||||
self.project().set_check_mode(self).to(mode);
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the system.
|
||||
///
|
||||
/// WARNING: Triggers a new revision, canceling other database handles. This can lead to deadlock.
|
||||
@@ -163,17 +172,28 @@ impl std::fmt::Debug for ProjectDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize))]
|
||||
pub enum CheckMode {
|
||||
/// Checks only the open files in the project.
|
||||
/// Checks the open files in the project.
|
||||
OpenFiles,
|
||||
|
||||
/// Checks all files in the project, ignoring the open file set.
|
||||
///
|
||||
/// This includes virtual files, such as those created by the language server.
|
||||
/// This includes virtual files, such as those opened in an editor.
|
||||
#[default]
|
||||
AllFiles,
|
||||
}
|
||||
|
||||
impl fmt::Display for CheckMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CheckMode::OpenFiles => write!(f, "open files"),
|
||||
CheckMode::AllFiles => write!(f, "all files"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores memory usage information.
|
||||
pub struct SalsaMemoryDump {
|
||||
total_fields: usize,
|
||||
@@ -389,12 +409,9 @@ impl IdeDb for ProjectDatabase {}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for ProjectDatabase {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
let Some(project) = &self.project else {
|
||||
return false;
|
||||
};
|
||||
|
||||
project.is_file_open(self, file)
|
||||
fn should_check_file(&self, file: File) -> bool {
|
||||
self.project
|
||||
.is_some_and(|project| project.should_check_file(self, file))
|
||||
}
|
||||
|
||||
fn rule_selection(&self, file: File) -> &RuleSelection {
|
||||
@@ -543,7 +560,7 @@ pub(crate) mod tests {
|
||||
|
||||
#[salsa::db]
|
||||
impl ty_python_semantic::Db for TestDb {
|
||||
fn is_file_open(&self, file: ruff_db::files::File) -> bool {
|
||||
fn should_check_file(&self, file: ruff_db::files::File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ use std::collections::BTreeSet;
|
||||
|
||||
use crate::walk::ProjectFilesWalker;
|
||||
use ruff_db::Db as _;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::file_revision::FileRevision;
|
||||
use ruff_db::files::{File, FileRootKind, Files};
|
||||
use ruff_db::system::SystemPath;
|
||||
use rustc_hash::FxHashSet;
|
||||
use salsa::Setter;
|
||||
@@ -57,12 +58,6 @@ impl ProjectDatabase {
|
||||
let mut synced_files = FxHashSet::default();
|
||||
let mut sync_recursively = BTreeSet::default();
|
||||
|
||||
let mut sync_path = |db: &mut ProjectDatabase, path: &SystemPath| {
|
||||
if synced_files.insert(path.to_path_buf()) {
|
||||
File::sync_path(db, path);
|
||||
}
|
||||
};
|
||||
|
||||
for change in changes {
|
||||
tracing::trace!("Handle change: {:?}", change);
|
||||
|
||||
@@ -92,12 +87,49 @@ impl ProjectDatabase {
|
||||
|
||||
match change {
|
||||
ChangeEvent::Changed { path, kind: _ } | ChangeEvent::Opened(path) => {
|
||||
sync_path(self, &path);
|
||||
if synced_files.insert(path.to_path_buf()) {
|
||||
let absolute =
|
||||
SystemPath::absolute(&path, self.system().current_directory());
|
||||
File::sync_path_only(self, &absolute);
|
||||
if let Some(root) = self.files().root(self, &absolute) {
|
||||
match root.kind_at_time_of_creation(self) {
|
||||
// When a file inside the root of
|
||||
// the project is changed, we don't
|
||||
// want to mark the entire root as
|
||||
// having changed too. In theory it
|
||||
// might make sense to, but at time
|
||||
// of writing, the file root revision
|
||||
// on a project is used to invalidate
|
||||
// the submodule files found within a
|
||||
// directory. If we bumped the revision
|
||||
// on every change within a project,
|
||||
// then this caching technique would be
|
||||
// effectively useless.
|
||||
//
|
||||
// It's plausible we should explore
|
||||
// a more robust cache invalidation
|
||||
// strategy that models more directly
|
||||
// what we care about. For example, by
|
||||
// keeping track of directories and
|
||||
// their direct children explicitly,
|
||||
// and then keying the submodule cache
|
||||
// off of that instead. ---AG
|
||||
FileRootKind::Project => {}
|
||||
FileRootKind::LibrarySearchPath => {
|
||||
root.set_revision(self).to(FileRevision::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChangeEvent::Created { kind, path } => {
|
||||
match kind {
|
||||
CreatedKind::File => sync_path(self, &path),
|
||||
CreatedKind::File => {
|
||||
if synced_files.insert(path.to_path_buf()) {
|
||||
File::sync_path(self, &path);
|
||||
}
|
||||
}
|
||||
CreatedKind::Directory | CreatedKind::Any => {
|
||||
sync_recursively.insert(path.clone());
|
||||
}
|
||||
@@ -138,7 +170,9 @@ impl ProjectDatabase {
|
||||
};
|
||||
|
||||
if is_file {
|
||||
sync_path(self, &path);
|
||||
if synced_files.insert(path.to_path_buf()) {
|
||||
File::sync_path(self, &path);
|
||||
}
|
||||
|
||||
if let Some(file) = self.files().try_system(self, &path) {
|
||||
project.remove_file(self, file);
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{IOErrorDiagnostic, Project};
|
||||
/// The implementation uses internal mutability to transition between the lazy and indexed state
|
||||
/// without triggering a new salsa revision. This is safe because the initial indexing happens on first access,
|
||||
/// so no query can be depending on the contents of the indexed files before that. All subsequent mutations to
|
||||
/// the indexed files must go through `IndexedMut`, which uses the Salsa setter `package.set_file_set` to
|
||||
/// the indexed files must go through `IndexedMut`, which uses the Salsa setter `project.set_file_set` to
|
||||
/// ensure that Salsa always knows when the set of indexed files have changed.
|
||||
#[derive(Debug)]
|
||||
pub struct IndexedFiles {
|
||||
@@ -280,7 +280,7 @@ mod tests {
|
||||
|
||||
// Calling files a second time should not dead-lock.
|
||||
// This can e.g. happen when `check_file` iterates over all files and
|
||||
// `is_file_open` queries the open files.
|
||||
// `should_check_file` queries the open files.
|
||||
let files_2 = project.file_set(&db).get();
|
||||
|
||||
match files_2 {
|
||||
|
||||
@@ -6,7 +6,7 @@ use files::{Index, Indexed, IndexedFiles};
|
||||
use metadata::settings::Settings;
|
||||
pub use metadata::{ProjectMetadata, ProjectMetadataError};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::files::{File, FileRootKind};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{SourceTextError, source_text};
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
@@ -14,6 +14,8 @@ use rustc_hash::FxHashSet;
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
use std::backtrace::BacktraceStatus;
|
||||
use std::collections::hash_set;
|
||||
use std::iter::FusedIterator;
|
||||
use std::panic::{AssertUnwindSafe, UnwindSafe};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
@@ -54,13 +56,10 @@ pub fn default_lints_registry() -> LintRegistry {
|
||||
#[salsa::input]
|
||||
#[derive(Debug)]
|
||||
pub struct Project {
|
||||
/// The files that are open in the project.
|
||||
///
|
||||
/// Setting the open files to a non-`None` value changes `check` to only check the
|
||||
/// open files rather than all files in the project.
|
||||
#[returns(as_deref)]
|
||||
/// The files that are open in the project, [`None`] if there are no open files.
|
||||
#[returns(ref)]
|
||||
#[default]
|
||||
open_fileset: Option<Arc<FxHashSet<File>>>,
|
||||
open_fileset: FxHashSet<File>,
|
||||
|
||||
/// The first-party files of this project.
|
||||
#[default]
|
||||
@@ -110,6 +109,13 @@ pub struct Project {
|
||||
/// Diagnostics that were generated when resolving the project settings.
|
||||
#[returns(deref)]
|
||||
settings_diagnostics: Vec<OptionDiagnostic>,
|
||||
|
||||
/// The mode in which the project should be checked.
|
||||
///
|
||||
/// This changes the behavior of `check` to either check only the open files or all files in
|
||||
/// the project including the virtual files that might exists in the editor.
|
||||
#[default]
|
||||
check_mode: CheckMode,
|
||||
}
|
||||
|
||||
/// A progress reporter.
|
||||
@@ -135,6 +141,13 @@ impl Project {
|
||||
pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Result<Self, ToSettingsError> {
|
||||
let (settings, diagnostics) = metadata.options().to_settings(db, metadata.root())?;
|
||||
|
||||
// This adds a file root for the project itself. This enables
|
||||
// tracking of when changes are made to the files in a project
|
||||
// at the directory level. At time of writing (2025-07-17),
|
||||
// this is used for caching completions for submodules.
|
||||
db.files()
|
||||
.try_add_root(db, metadata.root(), FileRootKind::Project);
|
||||
|
||||
let project = Project::builder(Box::new(metadata), Box::new(settings), diagnostics)
|
||||
.durability(Durability::MEDIUM)
|
||||
.open_fileset_durability(Durability::LOW)
|
||||
@@ -207,17 +220,20 @@ impl Project {
|
||||
self.reload_files(db);
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
/// Checks the project and its dependencies according to the project's check mode.
|
||||
pub(crate) fn check(
|
||||
self,
|
||||
db: &ProjectDatabase,
|
||||
mode: CheckMode,
|
||||
mut reporter: AssertUnwindSafe<&mut dyn ProgressReporter>,
|
||||
) -> Vec<Diagnostic> {
|
||||
let project_span = tracing::debug_span!("Project::check");
|
||||
let _span = project_span.enter();
|
||||
|
||||
tracing::debug!("Checking project '{name}'", name = self.name(db));
|
||||
tracing::debug!(
|
||||
"Checking {} in project '{name}'",
|
||||
self.check_mode(db),
|
||||
name = self.name(db)
|
||||
);
|
||||
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
diagnostics.extend(
|
||||
@@ -226,11 +242,7 @@ impl Project {
|
||||
.map(OptionDiagnostic::to_diagnostic),
|
||||
);
|
||||
|
||||
let files = match mode {
|
||||
CheckMode::OpenFiles => ProjectFiles::new(db, self),
|
||||
// TODO: Consider open virtual files as well
|
||||
CheckMode::AllFiles => ProjectFiles::Indexed(self.files(db)),
|
||||
};
|
||||
let files = ProjectFiles::new(db, self);
|
||||
reporter.set_files(files.len());
|
||||
|
||||
diagnostics.extend(
|
||||
@@ -240,6 +252,7 @@ impl Project {
|
||||
.map(IOErrorDiagnostic::to_diagnostic),
|
||||
);
|
||||
|
||||
let open_files = self.open_files(db);
|
||||
let check_start = ruff_db::Instant::now();
|
||||
let file_diagnostics = std::sync::Mutex::new(vec![]);
|
||||
|
||||
@@ -257,11 +270,30 @@ impl Project {
|
||||
tracing::debug_span!(parent: project_span, "check_file", ?file);
|
||||
let _entered = check_file_span.entered();
|
||||
|
||||
let result = check_file_impl(&db, file);
|
||||
file_diagnostics
|
||||
.lock()
|
||||
.unwrap()
|
||||
.extend(result.iter().map(Clone::clone));
|
||||
match check_file_impl(&db, file) {
|
||||
Ok(diagnostics) => {
|
||||
file_diagnostics
|
||||
.lock()
|
||||
.unwrap()
|
||||
.extend(diagnostics.iter().map(Clone::clone));
|
||||
|
||||
// This is outside `check_file_impl` to avoid that opening or closing
|
||||
// a file invalidates the `check_file_impl` query of every file!
|
||||
if !open_files.contains(&file) {
|
||||
// The module has already been parsed by `check_file_impl`.
|
||||
// We only retrieve it here so that we can call `clear` on it.
|
||||
let parsed = parsed_module(&db, file);
|
||||
|
||||
// Drop the AST now that we are done checking this file. It is not currently open,
|
||||
// so it is unlikely to be accessed again soon. If any queries need to access the AST
|
||||
// from across files, it will be re-parsed.
|
||||
parsed.clear();
|
||||
}
|
||||
}
|
||||
Err(io_error) => {
|
||||
file_diagnostics.lock().unwrap().push(io_error.clone());
|
||||
}
|
||||
}
|
||||
|
||||
reporter.report_file(&file);
|
||||
});
|
||||
@@ -284,16 +316,17 @@ impl Project {
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
if !self.is_file_open(db, file) {
|
||||
if !self.should_check_file(db, file) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
check_file_impl(db, file).iter().map(Clone::clone).collect()
|
||||
match check_file_impl(db, file) {
|
||||
Ok(diagnostics) => diagnostics.to_vec(),
|
||||
Err(diagnostic) => vec![diagnostic.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a file in the project.
|
||||
///
|
||||
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
|
||||
pub fn open_file(self, db: &mut dyn Db, file: File) {
|
||||
tracing::debug!("Opening file `{}`", file.path(db));
|
||||
|
||||
@@ -340,45 +373,40 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the open files in the project or `None` if the entire project should be checked.
|
||||
pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet<File>> {
|
||||
/// Returns the open files in the project or `None` if there are no open files.
|
||||
pub fn open_files(self, db: &dyn Db) -> &FxHashSet<File> {
|
||||
self.open_fileset(db)
|
||||
}
|
||||
|
||||
/// Sets the open files in the project.
|
||||
///
|
||||
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
|
||||
#[tracing::instrument(level = "debug", skip(self, db))]
|
||||
pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet<File>) {
|
||||
tracing::debug!("Set open project files (count: {})", open_files.len());
|
||||
|
||||
self.set_open_fileset(db).to(Some(Arc::new(open_files)));
|
||||
self.set_open_fileset(db).to(open_files);
|
||||
}
|
||||
|
||||
/// This takes the open files from the project and returns them.
|
||||
///
|
||||
/// This changes the behavior of `check` to check all files in the project instead of just the open files.
|
||||
fn take_open_files(self, db: &mut dyn Db) -> FxHashSet<File> {
|
||||
tracing::debug!("Take open project files");
|
||||
|
||||
// Salsa will cancel any pending queries and remove its own reference to `open_files`
|
||||
// so that the reference counter to `open_files` now drops to 1.
|
||||
let open_files = self.set_open_fileset(db).to(None);
|
||||
|
||||
if let Some(open_files) = open_files {
|
||||
Arc::try_unwrap(open_files).unwrap()
|
||||
} else {
|
||||
FxHashSet::default()
|
||||
}
|
||||
self.set_open_fileset(db).to(FxHashSet::default())
|
||||
}
|
||||
|
||||
/// Returns `true` if the file is open in the project.
|
||||
/// Returns `true` if the file should be checked.
|
||||
///
|
||||
/// A file is considered open when:
|
||||
/// * explicitly set as an open file using [`open_file`](Self::open_file)
|
||||
/// * It has a [`SystemPath`] and belongs to a package's `src` files
|
||||
/// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath)
|
||||
pub fn is_file_open(self, db: &dyn Db, file: File) -> bool {
|
||||
/// This depends on the project's check mode:
|
||||
/// * For [`OpenFiles`], it checks if the file is either explicitly set as an open file using
|
||||
/// [`open_file`] or a system virtual path
|
||||
/// * For [`AllFiles`], it checks if the file is either a system virtual path or a part of the
|
||||
/// indexed files in the project
|
||||
///
|
||||
/// [`open_file`]: Self::open_file
|
||||
/// [`OpenFiles`]: CheckMode::OpenFiles
|
||||
/// [`AllFiles`]: CheckMode::AllFiles
|
||||
pub fn should_check_file(self, db: &dyn Db, file: File) -> bool {
|
||||
let path = file.path(db);
|
||||
|
||||
// Try to return early to avoid adding a dependency on `open_files` or `file_set` which
|
||||
@@ -387,12 +415,12 @@ impl Project {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(open_files) = self.open_files(db) {
|
||||
open_files.contains(&file)
|
||||
} else if file.path(db).is_system_path() {
|
||||
self.files(db).contains(&file)
|
||||
} else {
|
||||
file.path(db).is_system_virtual_path()
|
||||
match self.check_mode(db) {
|
||||
CheckMode::OpenFiles => self.open_files(db).contains(&file),
|
||||
CheckMode::AllFiles => {
|
||||
// Virtual files are always checked.
|
||||
path.is_system_virtual_path() || self.files(db).contains(&file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,24 +497,29 @@ impl Project {
|
||||
self.set_file_set(db).to(IndexedFiles::lazy());
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the project's settings have any issues
|
||||
pub fn check_settings(&self, db: &dyn Db) -> Vec<Diagnostic> {
|
||||
self.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
|
||||
pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
|
||||
#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)]
|
||||
pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Result<Box<[Diagnostic]>, Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
|
||||
// Abort checking if there are IO errors.
|
||||
let source = source_text(db, file);
|
||||
|
||||
if let Some(read_error) = source.read_error() {
|
||||
diagnostics.push(
|
||||
IOErrorDiagnostic {
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}
|
||||
.to_diagnostic(),
|
||||
);
|
||||
return diagnostics.into_boxed_slice();
|
||||
return Err(IOErrorDiagnostic {
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}
|
||||
.to_diagnostic());
|
||||
}
|
||||
|
||||
let parsed = parsed_module(db, file);
|
||||
@@ -516,17 +549,6 @@ pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
|
||||
}
|
||||
}
|
||||
|
||||
if db
|
||||
.project()
|
||||
.open_fileset(db)
|
||||
.is_none_or(|files| !files.contains(&file))
|
||||
{
|
||||
// Drop the AST now that we are done checking this file. It is not currently open,
|
||||
// so it is unlikely to be accessed again soon. If any queries need to access the AST
|
||||
// from across files, it will be re-parsed.
|
||||
parsed.clear();
|
||||
}
|
||||
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| {
|
||||
diagnostic
|
||||
.primary_span()
|
||||
@@ -535,7 +557,7 @@ pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
|
||||
.start()
|
||||
});
|
||||
|
||||
diagnostics.into_boxed_slice()
|
||||
Ok(diagnostics.into_boxed_slice())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -546,24 +568,23 @@ enum ProjectFiles<'a> {
|
||||
|
||||
impl<'a> ProjectFiles<'a> {
|
||||
fn new(db: &'a dyn Db, project: Project) -> Self {
|
||||
if let Some(open_files) = project.open_files(db) {
|
||||
ProjectFiles::OpenFiles(open_files)
|
||||
} else {
|
||||
ProjectFiles::Indexed(project.files(db))
|
||||
match project.check_mode(db) {
|
||||
CheckMode::OpenFiles => ProjectFiles::OpenFiles(project.open_files(db)),
|
||||
CheckMode::AllFiles => ProjectFiles::Indexed(project.files(db)),
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostics(&self) -> &[IOErrorDiagnostic] {
|
||||
match self {
|
||||
ProjectFiles::OpenFiles(_) => &[],
|
||||
ProjectFiles::Indexed(indexed) => indexed.diagnostics(),
|
||||
ProjectFiles::Indexed(files) => files.diagnostics(),
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
match self {
|
||||
ProjectFiles::OpenFiles(open_files) => open_files.len(),
|
||||
ProjectFiles::Indexed(indexed) => indexed.len(),
|
||||
ProjectFiles::Indexed(files) => files.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,16 +596,14 @@ impl<'a> IntoIterator for &'a ProjectFiles<'a> {
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
match self {
|
||||
ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()),
|
||||
ProjectFiles::Indexed(indexed) => ProjectFilesIter::Indexed {
|
||||
files: indexed.into_iter(),
|
||||
},
|
||||
ProjectFiles::Indexed(files) => ProjectFilesIter::Indexed(files.into_iter()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ProjectFilesIter<'db> {
|
||||
OpenFiles(std::collections::hash_set::Iter<'db, File>),
|
||||
Indexed { files: files::IndexedIter<'db> },
|
||||
OpenFiles(hash_set::Iter<'db, File>),
|
||||
Indexed(files::IndexedIter<'db>),
|
||||
}
|
||||
|
||||
impl Iterator for ProjectFilesIter<'_> {
|
||||
@@ -593,11 +612,13 @@ impl Iterator for ProjectFilesIter<'_> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
ProjectFilesIter::OpenFiles(files) => files.next().copied(),
|
||||
ProjectFilesIter::Indexed { files } => files.next(),
|
||||
ProjectFilesIter::Indexed(files) => files.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for ProjectFilesIter<'_> {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IOErrorDiagnostic {
|
||||
file: Option<File>,
|
||||
@@ -754,10 +775,11 @@ mod tests {
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(
|
||||
check_file_impl(&db, file)
|
||||
.iter()
|
||||
.map(|diagnostic| diagnostic.primary_message().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Failed to read file: No such file or directory".to_string()]
|
||||
.as_ref()
|
||||
.unwrap_err()
|
||||
.primary_message()
|
||||
.to_string(),
|
||||
"Failed to read file: No such file or directory".to_string()
|
||||
);
|
||||
|
||||
let events = db.take_salsa_events();
|
||||
@@ -770,6 +792,8 @@ mod tests {
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(
|
||||
check_file_impl(&db, file)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|diagnostic| diagnostic.primary_message().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
|
||||
@@ -372,11 +372,11 @@ mod tests {
|
||||
|
||||
with_escaped_paths(|| {
|
||||
assert_ron_snapshot!(&project, @r#"
|
||||
ProjectMetadata(
|
||||
name: Name("app"),
|
||||
root: "/app",
|
||||
options: Options(),
|
||||
)
|
||||
ProjectMetadata(
|
||||
name: Name("app"),
|
||||
root: "/app",
|
||||
options: Options(),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
@@ -410,11 +410,11 @@ mod tests {
|
||||
|
||||
with_escaped_paths(|| {
|
||||
assert_ron_snapshot!(&project, @r#"
|
||||
ProjectMetadata(
|
||||
name: Name("backend"),
|
||||
root: "/app",
|
||||
options: Options(),
|
||||
)
|
||||
ProjectMetadata(
|
||||
name: Name("backend"),
|
||||
root: "/app",
|
||||
options: Options(),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
@@ -552,16 +552,16 @@ unclosed table, expected `]`
|
||||
|
||||
with_escaped_paths(|| {
|
||||
assert_ron_snapshot!(root, @r#"
|
||||
ProjectMetadata(
|
||||
name: Name("project-root"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
"#);
|
||||
ProjectMetadata(
|
||||
name: Name("project-root"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1806,7 +1806,7 @@ class Frozen:
|
||||
raise AttributeError("Attributes can not be modified")
|
||||
|
||||
instance = Frozen()
|
||||
instance.non_existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `non_existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
|
||||
instance.non_existing = 2 # error: [invalid-assignment] "Can not assign to unresolved attribute `non_existing` on type `Frozen`"
|
||||
instance.existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
|
||||
```
|
||||
|
||||
|
||||
@@ -415,8 +415,7 @@ frozen_instance = MyFrozenGeneric[int](1)
|
||||
frozen_instance.x = 2 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
When attempting to mutate an unresolved attribute on a frozen dataclass, only `unresolved-attribute`
|
||||
is emitted:
|
||||
Attempting to mutate an unresolved attribute on a frozen dataclass:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
@@ -425,7 +424,39 @@ from dataclasses import dataclass
|
||||
class MyFrozenClass: ...
|
||||
|
||||
frozen = MyFrozenClass()
|
||||
frozen.x = 2 # error: [unresolved-attribute]
|
||||
frozen.x = 2 # error: [invalid-assignment] "Can not assign to unresolved attribute `x` on type `MyFrozenClass`"
|
||||
```
|
||||
|
||||
A diagnostic is also emitted if a frozen dataclass is inherited, and an attempt is made to mutate an
|
||||
attribute in the child class:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MyFrozenClass:
|
||||
x: int = 1
|
||||
|
||||
class MyFrozenChildClass(MyFrozenClass): ...
|
||||
|
||||
frozen = MyFrozenChildClass()
|
||||
frozen.x = 2 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
The same diagnostic is emitted if a frozen dataclass is inherited, and an attempt is made to delete
|
||||
an attribute:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MyFrozenClass:
|
||||
x: int = 1
|
||||
|
||||
class MyFrozenChildClass(MyFrozenClass): ...
|
||||
|
||||
frozen = MyFrozenChildClass()
|
||||
del frozen.x # TODO this should emit an [invalid-assignment]
|
||||
```
|
||||
|
||||
### `match_args`
|
||||
@@ -467,7 +498,7 @@ class C:
|
||||
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
|
||||
|
||||
c = C(1)
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `instance_variable` on type `C`"
|
||||
c.instance_variable = 2
|
||||
```
|
||||
|
||||
|
||||
@@ -40,22 +40,92 @@ else:
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
|
||||
d = 1
|
||||
d = [1, 2, 3]
|
||||
|
||||
def delete():
|
||||
# TODO: this results in `UnboundLocalError`; we should emit `unresolved-reference`
|
||||
del d
|
||||
del d # error: [unresolved-reference] "Name `d` used when not defined"
|
||||
|
||||
delete()
|
||||
reveal_type(d) # revealed: Literal[1]
|
||||
reveal_type(d) # revealed: list[Unknown]
|
||||
|
||||
def delete_element():
|
||||
# When the `del` target isn't a name, it doesn't force local resolution.
|
||||
del d[0]
|
||||
print(d)
|
||||
|
||||
def delete_global():
|
||||
global d
|
||||
del d
|
||||
# We could lint that `d` is unbound in this trivial case, but because it's global we'd need to
|
||||
# be careful about false positives if `d` got reinitialized somehow in between the two `del`s.
|
||||
del d
|
||||
|
||||
delete_global()
|
||||
# The variable should have been removed, but we won't check it for now.
|
||||
reveal_type(d) # revealed: Literal[1]
|
||||
# Again, the variable should have been removed, but we don't check it.
|
||||
reveal_type(d) # revealed: list[Unknown]
|
||||
|
||||
def delete_nonlocal():
|
||||
e = 2
|
||||
|
||||
def delete_nonlocal_bad():
|
||||
del e # error: [unresolved-reference] "Name `e` used when not defined"
|
||||
|
||||
def delete_nonlocal_ok():
|
||||
nonlocal e
|
||||
del e
|
||||
# As with `global` above, we don't track that the nonlocal `e` is unbound.
|
||||
del e
|
||||
```
|
||||
|
||||
## `del` forces local resolution even if it's unreachable
|
||||
|
||||
Without a `global x` or `nonlocal x` declaration in `foo`, `del x` in `foo` causes `print(x)` in an
|
||||
inner function `bar` to resolve to `foo`'s binding, in this case an unresolved reference / unbound
|
||||
local error:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def foo():
|
||||
print(x) # error: [unresolved-reference] "Name `x` used when not defined"
|
||||
if False:
|
||||
# Assigning to `x` would have the same effect here.
|
||||
del x
|
||||
|
||||
def bar():
|
||||
print(x) # error: [unresolved-reference] "Name `x` used when not defined"
|
||||
```
|
||||
|
||||
## But `del` doesn't force local resolution of `global` or `nonlocal` variables
|
||||
|
||||
However, with `global x` in `foo`, `print(x)` in `bar` resolves in the global scope, despite the
|
||||
`del` in `foo`:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def foo():
|
||||
global x
|
||||
def bar():
|
||||
# allowed, refers to `x` in the global scope
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
bar()
|
||||
del x # allowed, deletes `x` in the global scope (though we don't track that)
|
||||
```
|
||||
|
||||
`nonlocal x` has a similar effect, if we add an extra `enclosing` scope to give it something to
|
||||
refer to:
|
||||
|
||||
```py
|
||||
def enclosing():
|
||||
x = 2
|
||||
def foo():
|
||||
nonlocal x
|
||||
def bar():
|
||||
# allowed, refers to `x` in `enclosing`
|
||||
reveal_type(x) # revealed: Unknown | Literal[2]
|
||||
bar()
|
||||
del x # allowed, deletes `x` in `enclosing` (though we don't track that)
|
||||
```
|
||||
|
||||
## Delete attributes
|
||||
|
||||
355
crates/ty_python_semantic/resources/mdtest/deprecated.md
Normal file
355
crates/ty_python_semantic/resources/mdtest/deprecated.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Tests for the `@deprecated` decorator
|
||||
|
||||
## Introduction
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
The decorator `@deprecated("some message")` can be applied to functions, methods, overloads, and
|
||||
classes. Uses of these items should subsequently produce a warning.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("use OtherClass")
|
||||
def myfunc(): ...
|
||||
|
||||
myfunc() # error: [deprecated] "use OtherClass"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("use BetterClass")
|
||||
class MyClass: ...
|
||||
|
||||
MyClass() # error: [deprecated] "use BetterClass"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
class MyClass:
|
||||
@deprecated("use something else")
|
||||
def afunc(): ...
|
||||
@deprecated("don't use this!")
|
||||
def amethod(self): ...
|
||||
|
||||
MyClass.afunc() # error: [deprecated] "use something else"
|
||||
MyClass().amethod() # error: [deprecated] "don't use this!"
|
||||
```
|
||||
|
||||
## Syntax
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
The typeshed declaration of the decorator is as follows:
|
||||
|
||||
```ignore
|
||||
class deprecated:
|
||||
message: LiteralString
|
||||
category: type[Warning] | None
|
||||
stacklevel: int
|
||||
def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
|
||||
def __call__(self, arg: _T, /) -> _T: ...
|
||||
```
|
||||
|
||||
Only the mandatory message string is of interest to static analysis, the other two affect only
|
||||
runtime behaviour.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated # error: [invalid-argument-type] "LiteralString"
|
||||
def invalid_deco(): ...
|
||||
|
||||
invalid_deco() # error: [missing-argument]
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated() # error: [missing-argument] "message"
|
||||
def invalid_deco(): ...
|
||||
|
||||
invalid_deco()
|
||||
```
|
||||
|
||||
The argument is supposed to be a LiteralString, and we can handle simple constant propagations like
|
||||
this:
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
x = "message"
|
||||
|
||||
@deprecated(x)
|
||||
def invalid_deco(): ...
|
||||
|
||||
invalid_deco() # error: [deprecated] "message"
|
||||
```
|
||||
|
||||
However sufficiently opaque LiteralStrings we can't resolve, and so we lose the message:
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated, LiteralString
|
||||
|
||||
def opaque() -> LiteralString:
|
||||
return "message"
|
||||
|
||||
@deprecated(opaque())
|
||||
def valid_deco(): ...
|
||||
|
||||
valid_deco() # error: [deprecated]
|
||||
```
|
||||
|
||||
Fully dynamic strings are technically allowed at runtime, but typeshed mandates that the input is a
|
||||
LiteralString, so we can/should emit a diagnostic for this:
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
def opaque() -> str:
|
||||
return "message"
|
||||
|
||||
@deprecated(opaque()) # error: [invalid-argument-type] "LiteralString"
|
||||
def dubious_deco(): ...
|
||||
|
||||
dubious_deco()
|
||||
```
|
||||
|
||||
Although we have no use for the other arguments, we should still error if they're wrong.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf"
|
||||
def invalid_deco(): ...
|
||||
|
||||
invalid_deco()
|
||||
```
|
||||
|
||||
And we should always handle correct ones fine.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("some message", category=DeprecationWarning, stacklevel=1)
|
||||
def valid_deco(): ...
|
||||
|
||||
valid_deco() # error: [deprecated] "some message"
|
||||
```
|
||||
|
||||
## Different Versions
|
||||
|
||||
There are 2 different sources of `@deprecated`: `warnings` and `typing_extensions`. The version in
|
||||
`warnings` was added in 3.13, the version in `typing_extensions` is a compatibility shim.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import warnings
|
||||
import typing_extensions
|
||||
|
||||
@warnings.deprecated("nope")
|
||||
def func1(): ...
|
||||
@typing_extensions.deprecated("nada")
|
||||
def func2(): ...
|
||||
|
||||
func1() # error: [deprecated] "nope"
|
||||
func2() # error: [deprecated] "nada"
|
||||
```
|
||||
|
||||
## Imports
|
||||
|
||||
### Direct Import Deprecated
|
||||
|
||||
Importing a deprecated item should produce a warning. Subsequent uses of the deprecated item
|
||||
shouldn't produce a warning.
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("Use OtherType instead")
|
||||
class DeprType: ...
|
||||
|
||||
@deprecated("Use other_func instead")
|
||||
def depr_func(): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
# error: [deprecated] "Use OtherType instead"
|
||||
# error: [deprecated] "Use other_func instead"
|
||||
from module import DeprType, depr_func
|
||||
|
||||
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
|
||||
DeprType() # error: [deprecated] "Use OtherType instead"
|
||||
depr_func() # error: [deprecated] "Use other_func instead"
|
||||
|
||||
def higher_order(x): ...
|
||||
|
||||
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
|
||||
higher_order(DeprType) # error: [deprecated] "Use OtherType instead"
|
||||
higher_order(depr_func) # error: [deprecated] "Use other_func instead"
|
||||
|
||||
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
|
||||
DeprType.__str__ # error: [deprecated] "Use OtherType instead"
|
||||
depr_func.__str__ # error: [deprecated] "Use other_func instead"
|
||||
```
|
||||
|
||||
### Non-Import Deprecated
|
||||
|
||||
If the items aren't imported and instead referenced using `module.item` then each use should produce
|
||||
a warning.
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("Use OtherType instead")
|
||||
class DeprType: ...
|
||||
|
||||
@deprecated("Use other_func instead")
|
||||
def depr_func(): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import module
|
||||
|
||||
module.DeprType() # error: [deprecated] "Use OtherType instead"
|
||||
module.depr_func() # error: [deprecated] "Use other_func instead"
|
||||
|
||||
def higher_order(x): ...
|
||||
|
||||
higher_order(module.DeprType) # error: [deprecated] "Use OtherType instead"
|
||||
higher_order(module.depr_func) # error: [deprecated] "Use other_func instead"
|
||||
|
||||
module.DeprType.__str__ # error: [deprecated] "Use OtherType instead"
|
||||
module.depr_func.__str__ # error: [deprecated] "Use other_func instead"
|
||||
```
|
||||
|
||||
### Star Import Deprecated
|
||||
|
||||
If the items are instead star-imported, then the actual uses should warn.
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("Use OtherType instead")
|
||||
class DeprType: ...
|
||||
|
||||
@deprecated("Use other_func instead")
|
||||
def depr_func(): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from module import *
|
||||
|
||||
DeprType() # error: [deprecated] "Use OtherType instead"
|
||||
depr_func() # error: [deprecated] "Use other_func instead"
|
||||
|
||||
def higher_order(x): ...
|
||||
|
||||
higher_order(DeprType) # error: [deprecated] "Use OtherType instead"
|
||||
higher_order(depr_func) # error: [deprecated] "Use other_func instead"
|
||||
|
||||
DeprType.__str__ # error: [deprecated] "Use OtherType instead"
|
||||
depr_func.__str__ # error: [deprecated] "Use other_func instead"
|
||||
```
|
||||
|
||||
## Aliases
|
||||
|
||||
Ideally a deprecated warning shouldn't transitively follow assignments, as you already had to "name"
|
||||
the deprecated symbol to assign it to something else. These kinds of diagnostics would therefore be
|
||||
redundant and annoying.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("Use OtherType instead")
|
||||
class DeprType: ...
|
||||
|
||||
@deprecated("Use other_func instead")
|
||||
def depr_func(): ...
|
||||
|
||||
alias_func = depr_func # error: [deprecated] "Use other_func instead"
|
||||
AliasClass = DeprType # error: [deprecated] "Use OtherType instead"
|
||||
|
||||
# TODO: these diagnostics ideally shouldn't fire
|
||||
alias_func() # error: [deprecated] "Use other_func instead"
|
||||
AliasClass() # error: [deprecated] "Use OtherType instead"
|
||||
```
|
||||
|
||||
## Dunders
|
||||
|
||||
If a dunder like `__add__` is deprecated, then the equivalent syntactic sugar like `+` should fire a
|
||||
diagnostic.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
class MyInt:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
@deprecated("MyInt `+` support is broken")
|
||||
def __add__(self, other):
|
||||
return MyInt(self.val + other.val)
|
||||
|
||||
x = MyInt(1)
|
||||
y = MyInt(2)
|
||||
z = x + y # TODO error: [deprecated] "MyInt `+` support is broken"
|
||||
```
|
||||
|
||||
## Overloads
|
||||
|
||||
Overloads can be deprecated, but only trigger warnings when invoked.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
from typing_extensions import overload
|
||||
|
||||
@overload
|
||||
@deprecated("strings are no longer supported")
|
||||
def f(x: str): ...
|
||||
@overload
|
||||
def f(x: int): ...
|
||||
def f(x):
|
||||
print(x)
|
||||
|
||||
f(1)
|
||||
f("hello") # TODO: error: [deprecated] "strings are no longer supported"
|
||||
```
|
||||
|
||||
If the actual impl is deprecated, the deprecation always fires.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
from typing_extensions import overload
|
||||
|
||||
@overload
|
||||
def f(x: str): ...
|
||||
@overload
|
||||
def f(x: int): ...
|
||||
@deprecated("unusable")
|
||||
def f(x):
|
||||
print(x)
|
||||
|
||||
f(1) # error: [deprecated] "unusable"
|
||||
f("hello") # error: [deprecated] "unusable"
|
||||
```
|
||||
@@ -72,7 +72,14 @@ reveal_type(my_bool(0)) # revealed: bool
|
||||
|
||||
## Truthy values
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
reveal_type(bool(1)) # revealed: Literal[True]
|
||||
reveal_type(bool((0,))) # revealed: Literal[True]
|
||||
reveal_type(bool("NON EMPTY")) # revealed: Literal[True]
|
||||
@@ -81,6 +88,42 @@ reveal_type(bool(True)) # revealed: Literal[True]
|
||||
def foo(): ...
|
||||
|
||||
reveal_type(bool(foo)) # revealed: Literal[True]
|
||||
|
||||
class SingleElementTupleSubclass(tuple[int]): ...
|
||||
|
||||
reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True]
|
||||
reveal_type(SingleElementTupleSubclass.__bool__) # revealed: (self: tuple[int], /) -> Literal[True]
|
||||
reveal_type(SingleElementTupleSubclass().__bool__) # revealed: () -> Literal[True]
|
||||
|
||||
# Unknown length, but we know the length is guaranteed to be >=2
|
||||
class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ...
|
||||
|
||||
reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True]
|
||||
reveal_type(MixedTupleSubclass.__bool__) # revealed: (self: tuple[int, *tuple[str, ...], bytes], /) -> Literal[True]
|
||||
reveal_type(MixedTupleSubclass().__bool__) # revealed: () -> Literal[True]
|
||||
|
||||
# Unknown length with an overridden `__bool__`:
|
||||
class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]):
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
reveal_type(bool(VariadicTupleSubclassWithDunderBoolOverride((1,)))) # revealed: Literal[True]
|
||||
reveal_type(VariadicTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True]
|
||||
|
||||
# revealed: bound method VariadicTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
|
||||
reveal_type(VariadicTupleSubclassWithDunderBoolOverride().__bool__)
|
||||
|
||||
# Same again but for a subclass of a fixed-length tuple:
|
||||
class EmptyTupleSubclassWithDunderBoolOverride(tuple[()]):
|
||||
# TODO: we should reject this override as a Liskov violation:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
reveal_type(bool(EmptyTupleSubclassWithDunderBoolOverride(()))) # revealed: Literal[True]
|
||||
reveal_type(EmptyTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True]
|
||||
|
||||
# revealed: bound method EmptyTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
|
||||
reveal_type(EmptyTupleSubclassWithDunderBoolOverride().__bool__)
|
||||
```
|
||||
|
||||
## Falsy values
|
||||
@@ -92,6 +135,12 @@ reveal_type(bool(None)) # revealed: Literal[False]
|
||||
reveal_type(bool("")) # revealed: Literal[False]
|
||||
reveal_type(bool(False)) # revealed: Literal[False]
|
||||
reveal_type(bool()) # revealed: Literal[False]
|
||||
|
||||
class EmptyTupleSubclass(tuple[()]): ...
|
||||
|
||||
reveal_type(bool(EmptyTupleSubclass())) # revealed: Literal[False]
|
||||
reveal_type(EmptyTupleSubclass.__bool__) # revealed: (self: tuple[()], /) -> Literal[False]
|
||||
reveal_type(EmptyTupleSubclass().__bool__) # revealed: () -> Literal[False]
|
||||
```
|
||||
|
||||
## Ambiguous values
|
||||
@@ -100,6 +149,13 @@ reveal_type(bool()) # revealed: Literal[False]
|
||||
reveal_type(bool([])) # revealed: bool
|
||||
reveal_type(bool({})) # revealed: bool
|
||||
reveal_type(bool(set())) # revealed: bool
|
||||
|
||||
class VariadicTupleSubclass(tuple[int, ...]): ...
|
||||
|
||||
def f(x: tuple[int, ...], y: VariadicTupleSubclass):
|
||||
reveal_type(bool(x)) # revealed: bool
|
||||
reveal_type(x.__bool__) # revealed: () -> bool
|
||||
reveal_type(y.__bool__) # revealed: () -> bool
|
||||
```
|
||||
|
||||
## `__bool__` returning `NoReturn`
|
||||
|
||||
@@ -65,6 +65,51 @@ reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
||||
reveal_type(len((*[], *{}))) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
Tuple subclasses:
|
||||
|
||||
```py
|
||||
class EmptyTupleSubclass(tuple[()]): ...
|
||||
class Length1TupleSubclass(tuple[int]): ...
|
||||
class Length2TupleSubclass(tuple[int, str]): ...
|
||||
class UnknownLengthTupleSubclass(tuple[int, ...]): ...
|
||||
|
||||
reveal_type(len(EmptyTupleSubclass())) # revealed: Literal[0]
|
||||
reveal_type(len(Length1TupleSubclass((1,)))) # revealed: Literal[1]
|
||||
reveal_type(len(Length2TupleSubclass((1, "foo")))) # revealed: Literal[2]
|
||||
reveal_type(len(UnknownLengthTupleSubclass((1, 2, 3)))) # revealed: int
|
||||
|
||||
reveal_type(tuple[int, int].__len__) # revealed: (self: tuple[int, int], /) -> Literal[2]
|
||||
reveal_type(tuple[int, ...].__len__) # revealed: (self: tuple[int, ...], /) -> int
|
||||
|
||||
def f(x: tuple[int, int], y: tuple[int, ...]):
|
||||
reveal_type(x.__len__) # revealed: () -> Literal[2]
|
||||
reveal_type(y.__len__) # revealed: () -> int
|
||||
|
||||
reveal_type(EmptyTupleSubclass.__len__) # revealed: (self: tuple[()], /) -> Literal[0]
|
||||
reveal_type(EmptyTupleSubclass().__len__) # revealed: () -> Literal[0]
|
||||
reveal_type(UnknownLengthTupleSubclass.__len__) # revealed: (self: tuple[int, ...], /) -> int
|
||||
reveal_type(UnknownLengthTupleSubclass().__len__) # revealed: () -> int
|
||||
```
|
||||
|
||||
If `__len__` is overridden, we use the overridden return type:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class UnknownLengthSubclassWithDunderLenOverridden(tuple[int, ...]):
|
||||
def __len__(self) -> Literal[42]:
|
||||
return 42
|
||||
|
||||
reveal_type(len(UnknownLengthSubclassWithDunderLenOverridden())) # revealed: Literal[42]
|
||||
|
||||
class FixedLengthSubclassWithDunderLenOverridden(tuple[int]):
|
||||
# TODO: we should complain about this as a Liskov violation (incompatible override)
|
||||
def __len__(self) -> Literal[42]:
|
||||
return 42
|
||||
|
||||
reveal_type(len(FixedLengthSubclassWithDunderLenOverridden((1,)))) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
### Lists, sets and dictionaries
|
||||
|
||||
```py
|
||||
|
||||
@@ -134,7 +134,6 @@ since these functions will never actually be called.
|
||||
|
||||
```py
|
||||
from typing import TYPE_CHECKING
|
||||
import typing
|
||||
|
||||
if TYPE_CHECKING:
|
||||
def f() -> int: ...
|
||||
@@ -200,9 +199,6 @@ if get_bool():
|
||||
if TYPE_CHECKING:
|
||||
if not TYPE_CHECKING:
|
||||
def n() -> str: ...
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
def o() -> str: ...
|
||||
```
|
||||
|
||||
## Conditional return type
|
||||
|
||||
@@ -3,37 +3,27 @@
|
||||
## `typing.TYPE_CHECKING`
|
||||
|
||||
This constant is `True` when in type-checking mode, `False` otherwise. The symbol is defined to be
|
||||
`False` at runtime. In typeshed, it is annotated as `bool`.
|
||||
`False` at runtime. In typeshed, it is annotated as `bool`. This test makes sure that we infer
|
||||
`Literal[True]` for it anyways.
|
||||
|
||||
### Basic
|
||||
|
||||
```py
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not TYPE_CHECKING:
|
||||
runtime = True
|
||||
|
||||
# type_checking is treated as unconditionally assigned.
|
||||
reveal_type(type_checking) # revealed: Literal[True]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(runtime) # revealed: Unknown
|
||||
```
|
||||
|
||||
### As module attribute
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not typing.TYPE_CHECKING:
|
||||
runtime = True
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
reveal_type(typing.TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
reveal_type(type_checking) # revealed: Literal[True]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(runtime) # revealed: Unknown
|
||||
### Aliased
|
||||
|
||||
Make sure that we still infer the correct type if the constant has been given a different name:
|
||||
|
||||
```py
|
||||
from typing import TYPE_CHECKING as TC
|
||||
|
||||
reveal_type(TC) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### `typing_extensions` re-export
|
||||
@@ -43,14 +33,7 @@ This should behave in the same way as `typing.TYPE_CHECKING`:
|
||||
```py
|
||||
from typing_extensions import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not TYPE_CHECKING:
|
||||
runtime = True
|
||||
|
||||
reveal_type(type_checking) # revealed: Literal[True]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(runtime) # revealed: Unknown
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## User-defined `TYPE_CHECKING`
|
||||
@@ -63,7 +46,7 @@ type checkers, e.g. mypy and pyright.
|
||||
|
||||
```py
|
||||
TYPE_CHECKING = False
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
if TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not TYPE_CHECKING:
|
||||
@@ -78,11 +61,11 @@ reveal_type(runtime) # revealed: Unknown
|
||||
### With a type annotation
|
||||
|
||||
We can also define `TYPE_CHECKING` with a type annotation. The type must be one to which `bool` can
|
||||
be assigned.
|
||||
be assigned. Even in this case, the type of `TYPE_CHECKING` is still inferred to be `Literal[True]`.
|
||||
|
||||
```py
|
||||
TYPE_CHECKING: bool = False
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
if TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not TYPE_CHECKING:
|
||||
@@ -101,21 +84,6 @@ reveal_type(runtime) # revealed: Unknown
|
||||
TYPE_CHECKING = False
|
||||
```
|
||||
|
||||
```py
|
||||
from constants import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not TYPE_CHECKING:
|
||||
runtime = True
|
||||
|
||||
reveal_type(type_checking) # revealed: Literal[True]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(runtime) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Importing user-defined `TYPE_CHECKING` from stub
|
||||
|
||||
`stub.pyi`:
|
||||
|
||||
```pyi
|
||||
@@ -125,16 +93,13 @@ TYPE_CHECKING: bool = ...
|
||||
```
|
||||
|
||||
```py
|
||||
from constants import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
|
||||
from stub import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not TYPE_CHECKING:
|
||||
runtime = True
|
||||
|
||||
reveal_type(type_checking) # revealed: Literal[True]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(runtime) # revealed: Unknown
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Invalid assignment to `TYPE_CHECKING`
|
||||
@@ -157,14 +122,12 @@ TYPE_CHECKING: int = 1
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: str = "str"
|
||||
|
||||
# error: [invalid-assignment]
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: str = False
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: Literal[False] = False
|
||||
|
||||
# error: [invalid-assignment]
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: Literal[True] = False
|
||||
```
|
||||
@@ -177,7 +140,6 @@ from typing import Literal
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: str
|
||||
|
||||
# error: [invalid-assignment]
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: str = False
|
||||
|
||||
|
||||
@@ -84,6 +84,52 @@ def f():
|
||||
x = "hello" # error: [invalid-assignment] "Object of type `Literal["hello"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## The types of `nonlocal` binding get unioned
|
||||
|
||||
Without a type declaration, we union the bindings in enclosing scopes to infer a type. But name
|
||||
resolution stops at the closest binding that isn't declared `nonlocal`, and we ignore bindings
|
||||
outside of that one:
|
||||
|
||||
```py
|
||||
def a():
|
||||
# This binding is shadowed in `b`, so we ignore it in inner scopes.
|
||||
x = 1
|
||||
|
||||
def b():
|
||||
x = 2
|
||||
|
||||
def c():
|
||||
nonlocal x
|
||||
x = 3
|
||||
|
||||
def d():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[3, 2]
|
||||
x = 4
|
||||
reveal_type(x) # revealed: Literal[4]
|
||||
|
||||
def e():
|
||||
reveal_type(x) # revealed: Unknown | Literal[4, 3, 2]
|
||||
```
|
||||
|
||||
However, currently the union of types that we build is incomplete. We walk parent scopes, but not
|
||||
sibling scopes, child scopes, second-cousin-once-removed scopes, etc:
|
||||
|
||||
```py
|
||||
def a():
|
||||
x = 1
|
||||
def b():
|
||||
nonlocal x
|
||||
x = 2
|
||||
|
||||
def c():
|
||||
def d():
|
||||
nonlocal x
|
||||
x = 3
|
||||
# TODO: This should include 2 and 3.
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Local variable bindings "look ahead" to any assignment in the current scope
|
||||
|
||||
The binding `x = 2` in `g` causes the earlier read of `x` to refer to `g`'s not-yet-initialized
|
||||
@@ -390,3 +436,13 @@ def f():
|
||||
nonlocal x
|
||||
x = 1
|
||||
```
|
||||
|
||||
## Narrowing nonlocal types to `Never` doesn't make them unbound
|
||||
|
||||
```py
|
||||
def foo():
|
||||
x: int = 1
|
||||
def bar():
|
||||
if isinstance(x, str):
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: deprecated.md - Tests for the `@deprecated` decorator - Introduction
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import deprecated
|
||||
2 |
|
||||
3 | @deprecated("use OtherClass")
|
||||
4 | def myfunc(): ...
|
||||
5 |
|
||||
6 | myfunc() # error: [deprecated] "use OtherClass"
|
||||
7 | from typing_extensions import deprecated
|
||||
8 |
|
||||
9 | @deprecated("use BetterClass")
|
||||
10 | class MyClass: ...
|
||||
11 |
|
||||
12 | MyClass() # error: [deprecated] "use BetterClass"
|
||||
13 | from typing_extensions import deprecated
|
||||
14 |
|
||||
15 | class MyClass:
|
||||
16 | @deprecated("use something else")
|
||||
17 | def afunc(): ...
|
||||
18 | @deprecated("don't use this!")
|
||||
19 | def amethod(self): ...
|
||||
20 |
|
||||
21 | MyClass.afunc() # error: [deprecated] "use something else"
|
||||
22 | MyClass().amethod() # error: [deprecated] "don't use this!"
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
warning[deprecated]: The function `myfunc` is deprecated
|
||||
--> src/mdtest_snippet.py:6:1
|
||||
|
|
||||
4 | def myfunc(): ...
|
||||
5 |
|
||||
6 | myfunc() # error: [deprecated] "use OtherClass"
|
||||
| ^^^^^^ use OtherClass
|
||||
7 | from typing_extensions import deprecated
|
||||
|
|
||||
info: rule `deprecated` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning[deprecated]: The class `MyClass` is deprecated
|
||||
--> src/mdtest_snippet.py:12:1
|
||||
|
|
||||
10 | class MyClass: ...
|
||||
11 |
|
||||
12 | MyClass() # error: [deprecated] "use BetterClass"
|
||||
| ^^^^^^^ use BetterClass
|
||||
13 | from typing_extensions import deprecated
|
||||
|
|
||||
info: rule `deprecated` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning[deprecated]: The function `afunc` is deprecated
|
||||
--> src/mdtest_snippet.py:21:9
|
||||
|
|
||||
19 | def amethod(self): ...
|
||||
20 |
|
||||
21 | MyClass.afunc() # error: [deprecated] "use something else"
|
||||
| ^^^^^ use something else
|
||||
22 | MyClass().amethod() # error: [deprecated] "don't use this!"
|
||||
|
|
||||
info: rule `deprecated` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning[deprecated]: The function `amethod` is deprecated
|
||||
--> src/mdtest_snippet.py:22:11
|
||||
|
|
||||
21 | MyClass.afunc() # error: [deprecated] "use something else"
|
||||
22 | MyClass().amethod() # error: [deprecated] "don't use this!"
|
||||
| ^^^^^^^ don't use this!
|
||||
|
|
||||
info: rule `deprecated` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: deprecated.md - Tests for the `@deprecated` decorator - Syntax
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import deprecated
|
||||
2 |
|
||||
3 | @deprecated # error: [invalid-argument-type] "LiteralString"
|
||||
4 | def invalid_deco(): ...
|
||||
5 |
|
||||
6 | invalid_deco() # error: [missing-argument]
|
||||
7 | from typing_extensions import deprecated
|
||||
8 |
|
||||
9 | @deprecated() # error: [missing-argument] "message"
|
||||
10 | def invalid_deco(): ...
|
||||
11 |
|
||||
12 | invalid_deco()
|
||||
13 | from typing_extensions import deprecated
|
||||
14 |
|
||||
15 | x = "message"
|
||||
16 |
|
||||
17 | @deprecated(x)
|
||||
18 | def invalid_deco(): ...
|
||||
19 |
|
||||
20 | invalid_deco() # error: [deprecated] "message"
|
||||
21 | from typing_extensions import deprecated, LiteralString
|
||||
22 |
|
||||
23 | def opaque() -> LiteralString:
|
||||
24 | return "message"
|
||||
25 |
|
||||
26 | @deprecated(opaque())
|
||||
27 | def valid_deco(): ...
|
||||
28 |
|
||||
29 | valid_deco() # error: [deprecated]
|
||||
30 | from typing_extensions import deprecated
|
||||
31 |
|
||||
32 | def opaque() -> str:
|
||||
33 | return "message"
|
||||
34 |
|
||||
35 | @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString"
|
||||
36 | def dubious_deco(): ...
|
||||
37 |
|
||||
38 | dubious_deco()
|
||||
39 | from typing_extensions import deprecated
|
||||
40 |
|
||||
41 | @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf"
|
||||
42 | def invalid_deco(): ...
|
||||
43 |
|
||||
44 | invalid_deco()
|
||||
45 | from typing_extensions import deprecated
|
||||
46 |
|
||||
47 | @deprecated("some message", category=DeprecationWarning, stacklevel=1)
|
||||
48 | def valid_deco(): ...
|
||||
49 |
|
||||
50 | valid_deco() # error: [deprecated] "some message"
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to class `deprecated` is incorrect
|
||||
--> src/mdtest_snippet.py:3:1
|
||||
|
|
||||
1 | from typing_extensions import deprecated
|
||||
2 |
|
||||
3 | @deprecated # error: [invalid-argument-type] "LiteralString"
|
||||
| ^^^^^^^^^^^ Expected `LiteralString`, found `def invalid_deco() -> Unknown`
|
||||
4 | def invalid_deco(): ...
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[missing-argument]: No argument provided for required parameter `arg` of bound method `__call__`
|
||||
--> src/mdtest_snippet.py:6:1
|
||||
|
|
||||
4 | def invalid_deco(): ...
|
||||
5 |
|
||||
6 | invalid_deco() # error: [missing-argument]
|
||||
| ^^^^^^^^^^^^^^
|
||||
7 | from typing_extensions import deprecated
|
||||
|
|
||||
info: rule `missing-argument` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[missing-argument]: No argument provided for required parameter `message` of class `deprecated`
|
||||
--> src/mdtest_snippet.py:9:2
|
||||
|
|
||||
7 | from typing_extensions import deprecated
|
||||
8 |
|
||||
9 | @deprecated() # error: [missing-argument] "message"
|
||||
| ^^^^^^^^^^^^
|
||||
10 | def invalid_deco(): ...
|
||||
|
|
||||
info: rule `missing-argument` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning[deprecated]: The function `invalid_deco` is deprecated
|
||||
--> src/mdtest_snippet.py:20:1
|
||||
|
|
||||
18 | def invalid_deco(): ...
|
||||
19 |
|
||||
20 | invalid_deco() # error: [deprecated] "message"
|
||||
| ^^^^^^^^^^^^ message
|
||||
21 | from typing_extensions import deprecated, LiteralString
|
||||
|
|
||||
info: rule `deprecated` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning[deprecated]: The function `valid_deco` is deprecated
|
||||
--> src/mdtest_snippet.py:29:1
|
||||
|
|
||||
27 | def valid_deco(): ...
|
||||
28 |
|
||||
29 | valid_deco() # error: [deprecated]
|
||||
| ^^^^^^^^^^
|
||||
30 | from typing_extensions import deprecated
|
||||
|
|
||||
info: rule `deprecated` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to class `deprecated` is incorrect
|
||||
--> src/mdtest_snippet.py:35:13
|
||||
|
|
||||
33 | return "message"
|
||||
34 |
|
||||
35 | @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString"
|
||||
| ^^^^^^^^ Expected `LiteralString`, found `str`
|
||||
36 | def dubious_deco(): ...
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[unknown-argument]: Argument `dsfsdf` does not match any known parameter of class `deprecated`
|
||||
--> src/mdtest_snippet.py:41:29
|
||||
|
|
||||
39 | from typing_extensions import deprecated
|
||||
40 |
|
||||
41 | @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf"
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
42 | def invalid_deco(): ...
|
||||
|
|
||||
info: rule `unknown-argument` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning[deprecated]: The function `valid_deco` is deprecated
|
||||
--> src/mdtest_snippet.py:50:1
|
||||
|
|
||||
48 | def valid_deco(): ...
|
||||
49 |
|
||||
50 | valid_deco() # error: [deprecated] "some message"
|
||||
| ^^^^^^^^^^ some message
|
||||
|
|
||||
info: rule `deprecated` is enabled by default
|
||||
|
||||
```
|
||||
@@ -29,16 +29,16 @@ error[invalid-argument-type]: Argument to function `loads` is incorrect
|
||||
| ^ Expected `str | bytes | bytearray`, found `Literal[5]`
|
||||
|
|
||||
info: Function defined here
|
||||
--> stdlib/json/__init__.pyi:219:5
|
||||
--> stdlib/json/__init__.pyi:218:5
|
||||
|
|
||||
217 | """
|
||||
218 |
|
||||
219 | def loads(
|
||||
216 | """
|
||||
217 |
|
||||
218 | def loads(
|
||||
| ^^^^^
|
||||
220 | s: str | bytes | bytearray,
|
||||
219 | s: str | bytes | bytearray,
|
||||
| -------------------------- Parameter declared here
|
||||
221 | *,
|
||||
222 | cls: type[JSONDecoder] | None = None,
|
||||
220 | *,
|
||||
221 | cls: type[JSONDecoder] | None = None,
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ error[invalid-syntax]
|
||||
--> src/mdtest_snippet.py:6:19
|
||||
|
|
||||
4 | async def f():
|
||||
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (synt...
|
||||
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax…
|
||||
6 | return {n: [x async for x in elements(n)] for n in range(3)}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
|
||||
7 | async def test():
|
||||
|
||||
@@ -41,7 +41,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/with/sync.md
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:6:6
|
||||
|
|
||||
5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and...
|
||||
5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `…
|
||||
6 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
7 | ...
|
||||
@@ -57,7 +57,7 @@ info: rule `invalid-context-manager` is enabled by default
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:13:6
|
||||
|
|
||||
12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an...
|
||||
12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and …
|
||||
13 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
14 | ...
|
||||
@@ -73,7 +73,7 @@ info: rule `invalid-context-manager` is enabled by default
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:20:6
|
||||
|
|
||||
19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an...
|
||||
19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and …
|
||||
20 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
21 | ...
|
||||
|
||||
@@ -154,6 +154,7 @@ the expression is not of statically known truthiness.
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert
|
||||
from typing import TYPE_CHECKING
|
||||
import sys
|
||||
|
||||
static_assert(True)
|
||||
@@ -173,6 +174,8 @@ static_assert("d" in "abc") # error: "Static assertion error: argument evaluate
|
||||
n = None
|
||||
static_assert(n is None)
|
||||
|
||||
static_assert(TYPE_CHECKING)
|
||||
|
||||
static_assert(sys.version_info >= (3, 6))
|
||||
```
|
||||
|
||||
|
||||
@@ -551,6 +551,11 @@ static_assert(is_subtype_of(Never, AlwaysFalsy))
|
||||
|
||||
### `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from ty_extensions import AlwaysTruthy, AlwaysFalsy, Intersection, Not, is_subtype_of, static_assert
|
||||
from typing_extensions import Literal, LiteralString
|
||||
@@ -588,6 +593,30 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]],
|
||||
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[AlwaysFalsy]))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy]))
|
||||
|
||||
class Length2TupleSubclass(tuple[int, str]): ...
|
||||
|
||||
static_assert(is_subtype_of(Length2TupleSubclass, AlwaysTruthy))
|
||||
|
||||
class EmptyTupleSubclass(tuple[()]): ...
|
||||
|
||||
static_assert(is_subtype_of(EmptyTupleSubclass, AlwaysFalsy))
|
||||
|
||||
class TupleSubclassWithAtLeastLength2(tuple[int, *tuple[str, ...], bytes]): ...
|
||||
|
||||
static_assert(is_subtype_of(TupleSubclassWithAtLeastLength2, AlwaysTruthy))
|
||||
|
||||
class UnknownLength(tuple[int, ...]): ...
|
||||
|
||||
static_assert(not is_subtype_of(UnknownLength, AlwaysTruthy))
|
||||
static_assert(not is_subtype_of(UnknownLength, AlwaysFalsy))
|
||||
|
||||
class Invalid(tuple[int, str]):
|
||||
# TODO: we should emit an error here (Liskov violation)
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
static_assert(is_subtype_of(Invalid, AlwaysFalsy))
|
||||
```
|
||||
|
||||
### `TypeGuard` and `TypeIs`
|
||||
|
||||
@@ -170,26 +170,37 @@ Assignments to attributes qualified with `Final` are also not allowed:
|
||||
```py
|
||||
from typing import Final
|
||||
|
||||
class C:
|
||||
FINAL_A: Final[int] = 1
|
||||
FINAL_B: Final = 1
|
||||
class Meta(type):
|
||||
META_FINAL_A: Final[int] = 1
|
||||
META_FINAL_B: Final = 1
|
||||
|
||||
class C(metaclass=Meta):
|
||||
CLASS_FINAL_A: Final[int] = 1
|
||||
CLASS_FINAL_B: Final = 1
|
||||
|
||||
def __init__(self):
|
||||
self.FINAL_C: Final[int] = 1
|
||||
self.FINAL_D: Final = 1
|
||||
self.INSTANCE_FINAL_A: Final[int] = 1
|
||||
self.INSTANCE_FINAL_B: Final = 1
|
||||
|
||||
# TODO: these should be errors (that mention `Final`)
|
||||
C.FINAL_A = 2
|
||||
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
|
||||
C.FINAL_B = 2
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type `<class 'C'>`"
|
||||
C.META_FINAL_A = 2
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_B` on type `<class 'C'>`"
|
||||
C.META_FINAL_B = 2
|
||||
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `<class 'C'>`"
|
||||
C.CLASS_FINAL_A = 2
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `<class 'C'>`"
|
||||
C.CLASS_FINAL_B = 2
|
||||
|
||||
# TODO: these should be errors (that mention `Final`)
|
||||
c = C()
|
||||
c.FINAL_A = 2
|
||||
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
|
||||
c.FINAL_B = 2
|
||||
c.FINAL_C = 2
|
||||
c.FINAL_D = 2
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `C`"
|
||||
c.CLASS_FINAL_A = 2
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `C`"
|
||||
c.CLASS_FINAL_B = 2
|
||||
# TODO: this should be an error
|
||||
c.INSTANCE_FINAL_A = 2
|
||||
# TODO: this should be an error
|
||||
c.INSTANCE_FINAL_B = 2
|
||||
```
|
||||
|
||||
## Mutability
|
||||
|
||||
@@ -5,7 +5,8 @@ use ruff_db::files::File;
|
||||
/// Database giving access to semantic information about a Python program.
|
||||
#[salsa::db]
|
||||
pub trait Db: SourceDb {
|
||||
fn is_file_open(&self, file: File) -> bool;
|
||||
/// Returns `true` if the file should be checked.
|
||||
fn should_check_file(&self, file: File) -> bool;
|
||||
|
||||
/// Resolves the rule selection for a given file.
|
||||
fn rule_selection(&self, file: File) -> &RuleSelection;
|
||||
@@ -114,7 +115,7 @@ pub(crate) mod tests {
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for TestDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
fn should_check_file(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@ pub use program::{
|
||||
pub use python_platform::PythonPlatform;
|
||||
pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel};
|
||||
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
|
||||
pub use types::definitions_for_name;
|
||||
pub use types::ide_support::ResolvedDefinition;
|
||||
pub use types::ide_support::{
|
||||
ResolvedDefinition, definitions_for_attribute, definitions_for_imported_symbol,
|
||||
definitions_for_name,
|
||||
};
|
||||
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
|
||||
|
||||
pub mod ast_node_ref;
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct Module {
|
||||
inner: Arc<ModuleInner>,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl Module {
|
||||
pub(crate) fn file_module(
|
||||
name: ModuleName,
|
||||
@@ -97,11 +98,16 @@ impl Module {
|
||||
///
|
||||
/// The names returned correspond to the "base" name of the module.
|
||||
/// That is, `{self.name}.{basename}` should give the full module name.
|
||||
pub fn all_submodules(&self, db: &dyn Db) -> Vec<Name> {
|
||||
self.all_submodules_inner(db).unwrap_or_default()
|
||||
pub fn all_submodules<'db>(&self, db: &'db dyn Db) -> &'db [Name] {
|
||||
self.clone()
|
||||
.all_submodules_inner(db, ())
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn all_submodules_inner(&self, db: &dyn Db) -> Option<Vec<Name>> {
|
||||
#[allow(clippy::ref_option, clippy::used_underscore_binding)]
|
||||
#[salsa::tracked(returns(ref))]
|
||||
fn all_submodules_inner(self, db: &dyn Db, _dummy: ()) -> Option<Vec<Name>> {
|
||||
fn is_submodule(
|
||||
is_dir: bool,
|
||||
is_file: bool,
|
||||
@@ -136,32 +142,42 @@ impl Module {
|
||||
);
|
||||
|
||||
Some(match path.parent()? {
|
||||
SystemOrVendoredPathRef::System(parent_directory) => db
|
||||
.system()
|
||||
.read_directory(parent_directory)
|
||||
.inspect_err(|err| {
|
||||
tracing::debug!(
|
||||
"Failed to read {parent_directory:?} when looking for \
|
||||
its possible submodules: {err}"
|
||||
);
|
||||
})
|
||||
.ok()?
|
||||
.flatten()
|
||||
.filter(|entry| {
|
||||
let ty = entry.file_type();
|
||||
let path = entry.path();
|
||||
is_submodule(
|
||||
ty.is_directory(),
|
||||
ty.is_file(),
|
||||
path.file_name(),
|
||||
path.extension(),
|
||||
)
|
||||
})
|
||||
.filter_map(|entry| {
|
||||
let stem = entry.path().file_stem()?;
|
||||
is_identifier(stem).then(|| Name::from(stem))
|
||||
})
|
||||
.collect(),
|
||||
SystemOrVendoredPathRef::System(parent_directory) => {
|
||||
// Read the revision on the corresponding file root to
|
||||
// register an explicit dependency on this directory
|
||||
// tree. When the revision gets bumped, the cache
|
||||
// that Salsa creates does for this routine will be
|
||||
// invalidated.
|
||||
if let Some(root) = db.files().root(db, parent_directory) {
|
||||
let _ = root.revision(db);
|
||||
}
|
||||
|
||||
db.system()
|
||||
.read_directory(parent_directory)
|
||||
.inspect_err(|err| {
|
||||
tracing::debug!(
|
||||
"Failed to read {parent_directory:?} when looking for \
|
||||
its possible submodules: {err}"
|
||||
);
|
||||
})
|
||||
.ok()?
|
||||
.flatten()
|
||||
.filter(|entry| {
|
||||
let ty = entry.file_type();
|
||||
let path = entry.path();
|
||||
is_submodule(
|
||||
ty.is_directory(),
|
||||
ty.is_file(),
|
||||
path.file_name(),
|
||||
path.extension(),
|
||||
)
|
||||
})
|
||||
.filter_map(|entry| {
|
||||
let stem = entry.path().file_stem()?;
|
||||
is_identifier(stem).then(|| Name::from(stem))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
SystemOrVendoredPathRef::Vendored(parent_directory) => db
|
||||
.vendored()
|
||||
.read_directory(parent_directory)
|
||||
@@ -259,6 +275,7 @@ pub enum KnownModule {
|
||||
UnittestMock,
|
||||
#[cfg(test)]
|
||||
Uuid,
|
||||
Warnings,
|
||||
}
|
||||
|
||||
impl KnownModule {
|
||||
@@ -278,6 +295,7 @@ impl KnownModule {
|
||||
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
|
||||
Self::TyExtensions => "ty_extensions",
|
||||
Self::ImportLib => "importlib",
|
||||
Self::Warnings => "warnings",
|
||||
#[cfg(test)]
|
||||
Self::UnittestMock => "unittest.mock",
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -754,10 +754,14 @@ fn place_by_id<'db>(
|
||||
// a diagnostic if we see it being modified externally. In type inference, we
|
||||
// can assign a "narrow" type to it even if it is not *declared*. This means, we
|
||||
// do not have to call [`widen_type_for_undeclared_public_symbol`].
|
||||
//
|
||||
// `TYPE_CHECKING` is a special variable that should only be assigned `False`
|
||||
// at runtime, but is always considered `True` in type checking.
|
||||
// See mdtest/known_constants.md#user-defined-type_checking for details.
|
||||
let is_considered_non_modifiable = place_table(db, scope)
|
||||
.place_expr(place_id)
|
||||
.expr
|
||||
.is_name_and(|name| matches!(name, "__slots__"));
|
||||
.is_name_and(|name| matches!(name, "__slots__" | "TYPE_CHECKING"));
|
||||
|
||||
if scope.file(db).is_stub(db) {
|
||||
// We generally trust module-level undeclared places in stubs and do not union
|
||||
|
||||
@@ -549,20 +549,14 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
fn build_predicate(&mut self, predicate_node: &ast::Expr) -> PredicateOrLiteral<'db> {
|
||||
// Some commonly used test expressions are eagerly evaluated as `true` or `false` here for
|
||||
// performance reasons. This list does not need to be exhaustive. More complex expressions
|
||||
// will still evaluate to the correct value during type-checking. (The one exception is
|
||||
// `TYPE_CHECKING`; we need to detect it here in order to handle it correctly in
|
||||
// conditions; in type inference it will resolve to its runtime value.)
|
||||
// Some commonly used test expressions are eagerly evaluated as `true`
|
||||
// or `false` here for performance reasons. This list does not need to
|
||||
// be exhaustive. More complex expressions will still evaluate to the
|
||||
// correct value during type-checking.
|
||||
fn resolve_to_literal(node: &ast::Expr) -> Option<bool> {
|
||||
match node {
|
||||
ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => Some(*value),
|
||||
ast::Expr::Name(ast::ExprName { id, .. }) if id == "TYPE_CHECKING" => Some(true),
|
||||
ast::Expr::Attribute(ast::ExprAttribute { attr, .. })
|
||||
if attr == "TYPE_CHECKING" =>
|
||||
{
|
||||
Some(true)
|
||||
}
|
||||
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(n),
|
||||
..
|
||||
@@ -2000,8 +1994,26 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
walk_stmt(self, stmt);
|
||||
for target in targets {
|
||||
if let Ok(target) = PlaceExpr::try_from(target) {
|
||||
let is_name = target.is_name();
|
||||
let place_id = self.add_place(PlaceExprWithFlags::new(target));
|
||||
self.current_place_table_mut().mark_place_used(place_id);
|
||||
let place_table = self.current_place_table_mut();
|
||||
if is_name {
|
||||
// `del x` behaves like an assignment in that it forces all references
|
||||
// to `x` in the current scope (including *prior* references) to refer
|
||||
// to the current scope's binding (unless `x` is declared `global` or
|
||||
// `nonlocal`). For example, this is an UnboundLocalError at runtime:
|
||||
//
|
||||
// ```py
|
||||
// x = 1
|
||||
// def foo():
|
||||
// print(x) # can't refer to global `x`
|
||||
// if False:
|
||||
// del x
|
||||
// foo()
|
||||
// ```
|
||||
place_table.mark_place_bound(place_id);
|
||||
}
|
||||
place_table.mark_place_used(place_id);
|
||||
self.delete_binding(place_id);
|
||||
}
|
||||
}
|
||||
@@ -2528,7 +2540,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
|
||||
}
|
||||
|
||||
fn report_semantic_error(&self, error: SemanticSyntaxError) {
|
||||
if self.db.is_file_open(self.file) {
|
||||
if self.db.should_check_file(self.file) {
|
||||
self.semantic_syntax_errors.borrow_mut().push(error);
|
||||
}
|
||||
}
|
||||
@@ -2759,12 +2771,14 @@ impl ExpressionsScopeMapBuilder {
|
||||
/// Returns if the expression is a `TYPE_CHECKING` expression.
|
||||
fn is_if_type_checking(expr: &ast::Expr) -> bool {
|
||||
matches!(expr, ast::Expr::Name(ast::ExprName { id, .. }) if id == "TYPE_CHECKING")
|
||||
|| matches!(expr, ast::Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "TYPE_CHECKING")
|
||||
}
|
||||
|
||||
/// Returns if the expression is a `not TYPE_CHECKING` expression.
|
||||
fn is_if_not_type_checking(expr: &ast::Expr) -> bool {
|
||||
matches!(expr, ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, .. }) if *op == ruff_python_ast::UnaryOp::Not
|
||||
&& is_if_type_checking(operand)
|
||||
&& matches!(
|
||||
&**operand,
|
||||
ast::Expr::Name(ast::ExprName { id, .. }) if id == "TYPE_CHECKING"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ pub struct Definitions<'db> {
|
||||
impl<'db> Definitions<'db> {
|
||||
pub(crate) fn single(definition: Definition<'db>) -> Self {
|
||||
Self {
|
||||
definitions: smallvec::smallvec![definition],
|
||||
definitions: smallvec::smallvec_inline![definition],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruff_index::{IndexVec, newtype_index};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
use rustc_hash::FxHasher;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::Db;
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
@@ -162,10 +162,10 @@ impl TryFrom<ast::ExprRef<'_>> for PlaceExpr {
|
||||
}
|
||||
|
||||
impl PlaceExpr {
|
||||
pub(crate) fn name(name: Name) -> Self {
|
||||
pub(crate) const fn name(name: Name) -> Self {
|
||||
Self {
|
||||
root_name: name,
|
||||
sub_segments: smallvec![],
|
||||
sub_segments: SmallVec::new_const(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user