Compare commits
1 Commits
0.12.5
...
micha/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
371f3f7340 |
30
.github/workflows/ci.yaml
vendored
30
.github/workflows/ci.yaml
vendored
@@ -143,12 +143,12 @@ jobs:
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
# NOTE: Do not exclude all Markdown files here, but rather use
|
||||
# specific exclude patterns like 'docs/**'), because tests for
|
||||
# 'ty' are written in Markdown.
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \
|
||||
':!**/*.md' \
|
||||
':crates/ty_python_semantic/resources/mdtest/**/*.md' \
|
||||
':!docs/**' \
|
||||
':!assets/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
@@ -238,13 +238,13 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
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@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
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@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
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@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # 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@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
- name: "Build tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -903,7 +903,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -936,7 +936,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
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@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # 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,10 +34,6 @@ 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.
|
||||
@@ -68,20 +64,20 @@ jobs:
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- name: Sync typeshed stubs
|
||||
run: |
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}"
|
||||
mkdir "ruff/${VENDORED_TYPESHED}"
|
||||
cp typeshed/README.md "ruff/${VENDORED_TYPESHED}"
|
||||
cp typeshed/LICENSE "ruff/${VENDORED_TYPESHED}"
|
||||
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
|
||||
|
||||
# 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/${VENDORED_TYPESHED}"
|
||||
cp typeshed/pyproject.toml ruff/crates/ty_vendored/vendor/typeshed
|
||||
|
||||
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"
|
||||
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
|
||||
cd ruff
|
||||
git checkout -b "${UPSTREAM_BRANCH}"
|
||||
git checkout -b typeshedbot/sync-typeshed
|
||||
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
|
||||
@@ -171,17 +167,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 "${VENDORED_TYPESHED}/stdlib" --config "${VENDORED_TYPESHED}/pyproject.toml" || true
|
||||
uvx black crates/ty_vendored/vendor/typeshed/stdlib --config crates/ty_vendored/vendor/typeshed/pyproject.toml || true
|
||||
git commit -am "Format codemodded docstrings" --allow-empty
|
||||
|
||||
rm "${VENDORED_TYPESHED}/pyproject.toml"
|
||||
rm crates/ty_vendored/vendor/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 "${UPSTREAM_BRANCH}" --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
|
||||
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 create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"
|
||||
|
||||
create-issue-on-failure:
|
||||
|
||||
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
@@ -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.4
|
||||
rev: v0.12.3
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,23 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
|
||||
- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
|
||||
- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
|
||||
- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
|
||||
- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
|
||||
|
||||
## 0.12.4
|
||||
|
||||
### Preview features
|
||||
|
||||
78
Cargo.lock
generated
78
Cargo.lock
generated
@@ -261,18 +261,6 @@ version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -1133,12 +1121,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -2566,12 +2548,6 @@ version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -2585,9 +2561,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
@@ -2734,7 +2710,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2851,6 +2827,7 @@ dependencies = [
|
||||
"anstyle",
|
||||
"arc-swap",
|
||||
"camino",
|
||||
"countme",
|
||||
"dashmap",
|
||||
"dunce",
|
||||
"etcetera",
|
||||
@@ -2985,7 +2962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3064,7 +3041,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
"rand 0.9.2",
|
||||
"rand 0.9.1",
|
||||
"ruff_diagnostics",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
@@ -3317,7 +3294,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3558,9 +3535,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.141"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3750,22 +3727,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -3791,12 +3769,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.20.0"
|
||||
@@ -4211,7 +4183,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
@@ -4220,10 +4191,11 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"salsa",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
"ty_project",
|
||||
"ty_python_semantic",
|
||||
"ty_vendored",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4257,6 +4229,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.9.2",
|
||||
"tracing",
|
||||
"ty_ide",
|
||||
"ty_python_semantic",
|
||||
"ty_vendored",
|
||||
]
|
||||
@@ -4267,7 +4240,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitvec",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"compact_str",
|
||||
@@ -4304,7 +4276,6 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thin-vec",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"ty_python_semantic",
|
||||
@@ -4320,13 +4291,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"crossbeam",
|
||||
"dunce",
|
||||
"insta",
|
||||
"jod-thread",
|
||||
"libc",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -4337,7 +4305,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -4597,7 +4564,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"rand 0.9.2",
|
||||
"rand 0.9.1",
|
||||
"uuid-macro-internal",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -5126,15 +5093,6 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
|
||||
@@ -57,9 +57,6 @@ assert_fs = { version = "1.1.0" }
|
||||
argfile = { version = "0.2.0" }
|
||||
bincode = { version = "2.0.0" }
|
||||
bitflags = { version = "2.5.0" }
|
||||
bitvec = { version = "1.0.1", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
bstr = { version = "1.9.1" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
camino = { version = "1.1.7" }
|
||||
@@ -166,7 +163,6 @@ strum_macros = { version = "0.27.0" }
|
||||
syn = { version = "2.0.55" }
|
||||
tempfile = { version = "3.9.0" }
|
||||
test-case = { version = "3.3.1" }
|
||||
thin-vec = { version = "0.2.14" }
|
||||
thiserror = { version = "2.0.0" }
|
||||
tikv-jemallocator = { version = "0.6.0" }
|
||||
toml = { version = "0.9.0" }
|
||||
|
||||
@@ -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.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.5/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.5
|
||||
rev: v0.12.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -454,7 +454,7 @@ impl LintCacheData {
|
||||
CacheMessage {
|
||||
rule,
|
||||
body: msg.body().to_string(),
|
||||
suggestion: msg.first_help_text().map(ToString::to_string),
|
||||
suggestion: msg.suggestion().map(ToString::to_string),
|
||||
range: msg.expect_range(),
|
||||
parent: msg.parent(),
|
||||
fix: msg.fix().cloned(),
|
||||
|
||||
@@ -264,7 +264,6 @@ impl Printer {
|
||||
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
|
||||
.with_show_source(self.format == OutputFormat::Full)
|
||||
.with_unsafe_fixes(self.unsafe_fixes)
|
||||
.with_preview(preview)
|
||||
.emit(writer, &diagnostics.inner, &context)?;
|
||||
|
||||
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
|
||||
|
||||
@@ -25,6 +25,7 @@ ty_static = { workspace = true }
|
||||
anstyle = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
filetime = { workspace = true }
|
||||
@@ -58,11 +59,6 @@ tempfile = { workspace = true }
|
||||
cache = ["ruff_cache"]
|
||||
junit = ["dep:quick-junit"]
|
||||
os = ["ignore", "dep:etcetera"]
|
||||
serde = [
|
||||
"camino/serde1",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
"ruff_diagnostics/serde",
|
||||
]
|
||||
serde = ["camino/serde1", "dep:serde", "dep:serde_json", "ruff_diagnostics/serde"]
|
||||
# Exposes testing utilities.
|
||||
testing = ["tracing-subscriber"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{fmt::Formatter, path::Path, sync::Arc};
|
||||
|
||||
use ruff_diagnostics::{Applicability, Fix};
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_source_file::{LineColumn, SourceCode, SourceFile};
|
||||
|
||||
use ruff_annotate_snippets::Level as AnnotateLevel;
|
||||
@@ -122,14 +122,7 @@ impl Diagnostic {
|
||||
/// directly. If callers want or need to avoid cloning the diagnostic
|
||||
/// message, then they can also pass a `DiagnosticMessage` directly.
|
||||
pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
|
||||
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message));
|
||||
}
|
||||
|
||||
/// Adds a "help" sub-diagnostic with the given message.
|
||||
///
|
||||
/// See the closely related [`Diagnostic::info`] method for more details.
|
||||
pub fn help<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
|
||||
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Help, message));
|
||||
self.sub(SubDiagnostic::new(Severity::Info, message));
|
||||
}
|
||||
|
||||
/// Adds a "sub" diagnostic to this diagnostic.
|
||||
@@ -384,15 +377,9 @@ impl Diagnostic {
|
||||
self.primary_message()
|
||||
}
|
||||
|
||||
/// Returns the message of the first sub-diagnostic with a `Help` severity.
|
||||
///
|
||||
/// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
|
||||
/// general this is not the guaranteed meaning of such a message.
|
||||
pub fn first_help_text(&self) -> Option<&str> {
|
||||
self.sub_diagnostics()
|
||||
.iter()
|
||||
.find(|sub| matches!(sub.inner.severity, SubDiagnosticSeverity::Help))
|
||||
.map(|sub| sub.inner.message.as_str())
|
||||
/// Returns the fix suggestion for the violation.
|
||||
pub fn suggestion(&self) -> Option<&str> {
|
||||
self.primary_annotation()?.get_message()
|
||||
}
|
||||
|
||||
/// Returns the URL for the rule documentation, if it exists.
|
||||
@@ -578,10 +565,7 @@ impl SubDiagnostic {
|
||||
/// Callers can pass anything that implements `std::fmt::Display`
|
||||
/// directly. If callers want or need to avoid cloning the diagnostic
|
||||
/// message, then they can also pass a `DiagnosticMessage` directly.
|
||||
pub fn new<'a>(
|
||||
severity: SubDiagnosticSeverity,
|
||||
message: impl IntoDiagnosticMessage + 'a,
|
||||
) -> SubDiagnostic {
|
||||
pub fn new<'a>(severity: Severity, message: impl IntoDiagnosticMessage + 'a) -> SubDiagnostic {
|
||||
let inner = Box::new(SubDiagnosticInner {
|
||||
severity,
|
||||
message: message.into_diagnostic_message(),
|
||||
@@ -659,7 +643,7 @@ impl SubDiagnostic {
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
struct SubDiagnosticInner {
|
||||
severity: SubDiagnosticSeverity,
|
||||
severity: Severity,
|
||||
message: DiagnosticMessage,
|
||||
annotations: Vec<Annotation>,
|
||||
}
|
||||
@@ -1186,32 +1170,6 @@ impl Severity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`Severity`] but exclusively for sub-diagnostics.
|
||||
///
|
||||
/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
|
||||
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
|
||||
/// deleted and the two combined again.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
|
||||
pub enum SubDiagnosticSeverity {
|
||||
Help,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
impl SubDiagnosticSeverity {
|
||||
fn to_annotate(self) -> AnnotateLevel {
|
||||
match self {
|
||||
SubDiagnosticSeverity::Help => AnnotateLevel::Help,
|
||||
SubDiagnosticSeverity::Info => AnnotateLevel::Info,
|
||||
SubDiagnosticSeverity::Warning => AnnotateLevel::Warning,
|
||||
SubDiagnosticSeverity::Error => AnnotateLevel::Error,
|
||||
SubDiagnosticSeverity::Fatal => AnnotateLevel::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for rendering diagnostics.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DisplayDiagnosticConfig {
|
||||
@@ -1238,15 +1196,6 @@ pub struct DisplayDiagnosticConfig {
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
)]
|
||||
preview: bool,
|
||||
/// Whether to hide the real `Severity` of diagnostics.
|
||||
///
|
||||
/// This is intended for temporary use by Ruff, which only has a single `error` severity at the
|
||||
/// moment. We should be able to remove this option when Ruff gets more severities.
|
||||
hide_severity: bool,
|
||||
/// Whether to show the availability of a fix in a diagnostic.
|
||||
show_fix_status: bool,
|
||||
/// The lowest applicability that should be shown when reporting diagnostics.
|
||||
fix_applicability: Applicability,
|
||||
}
|
||||
|
||||
impl DisplayDiagnosticConfig {
|
||||
@@ -1275,35 +1224,6 @@ impl DisplayDiagnosticConfig {
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to hide a diagnostic's severity or not.
|
||||
pub fn hide_severity(self, yes: bool) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
hide_severity: yes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to show a fix's availability or not.
|
||||
pub fn show_fix_status(self, yes: bool) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
show_fix_status: yes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the lowest fix applicability that should be shown.
|
||||
///
|
||||
/// In other words, an applicability of `Safe` (the default) would suppress showing fixes or fix
|
||||
/// availability for unsafe or display-only fixes.
|
||||
///
|
||||
/// Note that this option is currently ignored when `hide_severity` is false.
|
||||
pub fn fix_applicability(self, applicability: Applicability) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
fix_applicability: applicability,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DisplayDiagnosticConfig {
|
||||
@@ -1313,9 +1233,6 @@ impl Default for DisplayDiagnosticConfig {
|
||||
color: false,
|
||||
context: 2,
|
||||
preview: false,
|
||||
hide_severity: false,
|
||||
show_fix_status: false,
|
||||
fix_applicability: Applicability::Safe,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::diagnostic::stylesheet::DiagnosticStylesheet;
|
||||
use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled};
|
||||
use crate::{
|
||||
Db,
|
||||
files::File,
|
||||
@@ -18,17 +18,14 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig,
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
|
||||
SubDiagnostic, UnifiedFile,
|
||||
};
|
||||
|
||||
use azure::AzureRenderer;
|
||||
use concise::ConciseRenderer;
|
||||
use pylint::PylintRenderer;
|
||||
|
||||
mod azure;
|
||||
mod concise;
|
||||
mod full;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json;
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -107,7 +104,48 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.config.format {
|
||||
DiagnosticFormat::Concise => {
|
||||
ConciseRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
for diag in self.diagnostics {
|
||||
let (severity, severity_style) = match diag.severity() {
|
||||
Severity::Info => ("info", stylesheet.info),
|
||||
Severity::Warning => ("warning", stylesheet.warning),
|
||||
Severity::Error => ("error", stylesheet.error),
|
||||
Severity::Fatal => ("fatal", stylesheet.error),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{severity}[{id}]",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(span) = diag.primary_span() {
|
||||
write!(
|
||||
f,
|
||||
" {path}",
|
||||
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
write!(
|
||||
f,
|
||||
":{line}:{col}",
|
||||
line = fmt_styled(start.line, stylesheet.emphasis),
|
||||
col = fmt_styled(start.column, stylesheet.emphasis),
|
||||
)?;
|
||||
}
|
||||
write!(f, ":")?;
|
||||
}
|
||||
writeln!(f, " {message}", message = diag.concise_message())?;
|
||||
}
|
||||
}
|
||||
DiagnosticFormat::Full => {
|
||||
let stylesheet = if self.config.color {
|
||||
@@ -218,7 +256,7 @@ impl<'a> Resolved<'a> {
|
||||
/// both.)
|
||||
#[derive(Debug)]
|
||||
struct ResolvedDiagnostic<'a> {
|
||||
level: AnnotateLevel,
|
||||
severity: Severity,
|
||||
id: Option<String>,
|
||||
message: String,
|
||||
annotations: Vec<ResolvedAnnotation<'a>>,
|
||||
@@ -243,7 +281,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
let id = Some(diag.inner.id.to_string());
|
||||
let message = diag.inner.message.as_str().to_string();
|
||||
ResolvedDiagnostic {
|
||||
level: diag.inner.severity.to_annotate(),
|
||||
severity: diag.inner.severity,
|
||||
id,
|
||||
message,
|
||||
annotations,
|
||||
@@ -266,7 +304,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
})
|
||||
.collect();
|
||||
ResolvedDiagnostic {
|
||||
level: diag.inner.severity.to_annotate(),
|
||||
severity: diag.inner.severity,
|
||||
id: None,
|
||||
message: diag.inner.message.as_str().to_string(),
|
||||
annotations,
|
||||
@@ -333,7 +371,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
snippets_by_input
|
||||
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
|
||||
RenderableDiagnostic {
|
||||
level: self.level,
|
||||
severity: self.severity,
|
||||
id: self.id.as_deref(),
|
||||
message: &self.message,
|
||||
snippets_by_input,
|
||||
@@ -421,7 +459,7 @@ struct Renderable<'r> {
|
||||
#[derive(Debug)]
|
||||
struct RenderableDiagnostic<'r> {
|
||||
/// The severity of the diagnostic.
|
||||
level: AnnotateLevel,
|
||||
severity: Severity,
|
||||
/// The ID of the diagnostic. The ID can usually be used on the CLI or in a
|
||||
/// config file to change the severity of a lint.
|
||||
///
|
||||
@@ -440,6 +478,7 @@ struct RenderableDiagnostic<'r> {
|
||||
impl RenderableDiagnostic<'_> {
|
||||
/// Convert this to an "annotate" snippet.
|
||||
fn to_annotate(&self) -> AnnotateMessage<'_> {
|
||||
let level = self.severity.to_annotate();
|
||||
let snippets = self.snippets_by_input.iter().flat_map(|snippets| {
|
||||
let path = snippets.path;
|
||||
snippets
|
||||
@@ -447,7 +486,7 @@ impl RenderableDiagnostic<'_> {
|
||||
.iter()
|
||||
.map(|snippet| snippet.to_annotate(path))
|
||||
});
|
||||
let mut message = self.level.title(self.message);
|
||||
let mut message = level.title(self.message);
|
||||
if let Some(id) = self.id {
|
||||
message = message.id(id);
|
||||
}
|
||||
@@ -823,12 +862,9 @@ fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
|
||||
use crate::diagnostic::{
|
||||
Annotation, DiagnosticId, IntoDiagnosticMessage, SecondaryCode, Severity, Span,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use crate::diagnostic::{Annotation, DiagnosticId, SecondaryCode, Severity, Span};
|
||||
use crate::files::system_path_to_file;
|
||||
use crate::system::{DbWithWritableSystem, SystemPath};
|
||||
use crate::tests::TestDb;
|
||||
@@ -1512,7 +1548,7 @@ watermelon
|
||||
|
||||
let mut diag = env.err().primary("animals", "3", "3", "").build();
|
||||
diag.sub(
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
|
||||
env.sub_builder(Severity::Info, "this is a helpful note")
|
||||
.build(),
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
@@ -1541,15 +1577,15 @@ watermelon
|
||||
|
||||
let mut diag = env.err().primary("animals", "3", "3", "").build();
|
||||
diag.sub(
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
|
||||
env.sub_builder(Severity::Info, "this is a helpful note")
|
||||
.build(),
|
||||
);
|
||||
diag.sub(
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "another helpful note")
|
||||
env.sub_builder(Severity::Info, "another helpful note")
|
||||
.build(),
|
||||
);
|
||||
diag.sub(
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "and another helpful note")
|
||||
env.sub_builder(Severity::Info, "and another helpful note")
|
||||
.build(),
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
@@ -2271,27 +2307,6 @@ watermelon
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Hide diagnostic severity when rendering.
|
||||
pub(super) fn hide_severity(&mut self, yes: bool) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.hide_severity(yes);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Show fix availability when rendering.
|
||||
pub(super) fn show_fix_status(&mut self, yes: bool) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.show_fix_status(yes);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// The lowest fix applicability to show when rendering.
|
||||
pub(super) fn fix_applicability(&mut self, applicability: Applicability) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.fix_applicability(applicability);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Add a file with the given path and contents to this environment.
|
||||
pub(super) fn add(&mut self, path: &str, contents: &str) {
|
||||
let path = SystemPath::new(path);
|
||||
@@ -2355,7 +2370,7 @@ watermelon
|
||||
/// sub-diagnostic with "error" severity and canned values for
|
||||
/// its identifier and message.
|
||||
fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> {
|
||||
self.sub_builder(SubDiagnosticSeverity::Warning, "sub-diagnostic message")
|
||||
self.sub_builder(Severity::Warning, "sub-diagnostic message")
|
||||
}
|
||||
|
||||
/// Returns a builder for tersely constructing diagnostics.
|
||||
@@ -2376,11 +2391,7 @@ watermelon
|
||||
}
|
||||
|
||||
/// Returns a builder for tersely constructing sub-diagnostics.
|
||||
fn sub_builder(
|
||||
&mut self,
|
||||
severity: SubDiagnosticSeverity,
|
||||
message: &str,
|
||||
) -> SubDiagnosticBuilder<'_> {
|
||||
fn sub_builder(&mut self, severity: Severity, message: &str) -> SubDiagnosticBuilder<'_> {
|
||||
let subdiag = SubDiagnostic::new(severity, message);
|
||||
SubDiagnosticBuilder { env: self, subdiag }
|
||||
}
|
||||
@@ -2483,12 +2494,6 @@ watermelon
|
||||
self.diag.set_noqa_offset(noqa_offset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a "help" sub-diagnostic with the given message.
|
||||
fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
|
||||
self.diag.help(message);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper builder for tersely populating a `SubDiagnostic`.
|
||||
@@ -2595,8 +2600,7 @@ def fibonacci(n):
|
||||
|
||||
let diagnostics = vec![
|
||||
env.builder("unused-import", Severity::Error, "`os` imported but unused")
|
||||
.primary("fib.py", "1:7", "1:9", "")
|
||||
.help("Remove unused import: `os`")
|
||||
.primary("fib.py", "1:7", "1:9", "Remove unused import: `os`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(0),
|
||||
@@ -2609,8 +2613,12 @@ def fibonacci(n):
|
||||
Severity::Error,
|
||||
"Local variable `x` is assigned to but never used",
|
||||
)
|
||||
.primary("fib.py", "6:4", "6:5", "")
|
||||
.help("Remove assignment to unused variable `x`")
|
||||
.primary(
|
||||
"fib.py",
|
||||
"6:4",
|
||||
"6:5",
|
||||
"Remove assignment to unused variable `x`",
|
||||
)
|
||||
.secondary_code("F841")
|
||||
.fix(Fix::unsafe_edit(Edit::deletion(
|
||||
TextSize::from(94),
|
||||
@@ -2657,25 +2665,6 @@ if call(foo
|
||||
}
|
||||
|
||||
/// Create Ruff-style diagnostics for testing the various output formats for a notebook.
|
||||
///
|
||||
/// The concatenated cells look like this:
|
||||
///
|
||||
/// ```python
|
||||
/// # cell 1
|
||||
/// import os
|
||||
/// # cell 2
|
||||
/// import math
|
||||
///
|
||||
/// print('hello world')
|
||||
/// # cell 3
|
||||
/// def foo():
|
||||
/// print()
|
||||
/// x = 1
|
||||
/// ```
|
||||
///
|
||||
/// The first diagnostic is on the unused `os` import with location cell 1, row 2, column 8
|
||||
/// (`cell 1:2:8`). The second diagnostic is the unused `math` import at `cell 2:2:8`, and the
|
||||
/// third diagnostic is an unfixable unused variable at `cell 3:4:5`.
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
@@ -2731,8 +2720,7 @@ if call(foo
|
||||
|
||||
let diagnostics = vec![
|
||||
env.builder("unused-import", Severity::Error, "`os` imported but unused")
|
||||
.primary("notebook.ipynb", "2:7", "2:9", "")
|
||||
.help("Remove unused import: `os`")
|
||||
.primary("notebook.ipynb", "2:7", "2:9", "Remove unused import: `os`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(9),
|
||||
@@ -2745,8 +2733,12 @@ if call(foo
|
||||
Severity::Error,
|
||||
"`math` imported but unused",
|
||||
)
|
||||
.primary("notebook.ipynb", "4:7", "4:11", "")
|
||||
.help("Remove unused import: `math`")
|
||||
.primary(
|
||||
"notebook.ipynb",
|
||||
"4:7",
|
||||
"4:11",
|
||||
"Remove unused import: `math`",
|
||||
)
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(28),
|
||||
@@ -2759,8 +2751,12 @@ if call(foo
|
||||
Severity::Error,
|
||||
"Local variable `x` is assigned to but never used",
|
||||
)
|
||||
.primary("notebook.ipynb", "10:4", "10:5", "")
|
||||
.help("Remove assignment to unused variable `x`")
|
||||
.primary(
|
||||
"notebook.ipynb",
|
||||
"10:4",
|
||||
"10:5",
|
||||
"Remove assignment to unused variable `x`",
|
||||
)
|
||||
.secondary_code("F841")
|
||||
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(94),
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
use crate::diagnostic::{
|
||||
Diagnostic, DisplayDiagnosticConfig, Severity,
|
||||
stylesheet::{DiagnosticStylesheet, fmt_styled},
|
||||
};
|
||||
|
||||
use super::FileResolver;
|
||||
|
||||
pub(super) struct ConciseRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl<'a> ConciseRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig) -> Self {
|
||||
Self { resolver, config }
|
||||
}
|
||||
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
let sep = fmt_styled(":", stylesheet.separator);
|
||||
for diag in diagnostics {
|
||||
if let Some(span) = diag.primary_span() {
|
||||
write!(
|
||||
f,
|
||||
"{path}",
|
||||
path = fmt_styled(
|
||||
span.file().relative_path(self.resolver).to_string_lossy(),
|
||||
stylesheet.emphasis
|
||||
)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
if let Some(notebook_index) = self.resolver.notebook_index(span.file()) {
|
||||
write!(
|
||||
f,
|
||||
"{sep}cell {cell}{sep}{line}{sep}{col}",
|
||||
cell = notebook_index.cell(start.line).unwrap_or_default(),
|
||||
line = notebook_index.cell_row(start.line).unwrap_or_default(),
|
||||
col = start.column,
|
||||
)?;
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{sep}{line}{sep}{col}",
|
||||
line = start.line,
|
||||
col = start.column,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
write!(f, "{sep} ")?;
|
||||
}
|
||||
if self.config.hide_severity {
|
||||
if let Some(code) = diag.secondary_code() {
|
||||
write!(
|
||||
f,
|
||||
"{code} ",
|
||||
code = fmt_styled(code, stylesheet.secondary_code)
|
||||
)?;
|
||||
}
|
||||
if self.config.show_fix_status {
|
||||
if let Some(fix) = diag.fix() {
|
||||
// Do not display an indicator for inapplicable fixes
|
||||
if fix.applies(self.config.fix_applicability) {
|
||||
write!(f, "[{fix}] ", fix = fmt_styled("*", stylesheet.separator))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (severity, severity_style) = match diag.severity() {
|
||||
Severity::Info => ("info", stylesheet.info),
|
||||
Severity::Warning => ("warning", stylesheet.warning),
|
||||
Severity::Error => ("error", stylesheet.error),
|
||||
Severity::Fatal => ("fatal", stylesheet.error),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{severity}[{id}] ",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(f, "{message}", message = diag.concise_message())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_diagnostics::Applicability;
|
||||
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{
|
||||
TestEnvironment, create_diagnostics, create_notebook_diagnostics,
|
||||
create_syntax_error_diagnostics,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: error[unused-import] `os` imported but unused
|
||||
fib.py:6:5: error[unused-variable] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: error[undefined-name] Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes() {
|
||||
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: F401 [*] `os` imported but unused
|
||||
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes_preview() {
|
||||
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
env.preview(true);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: F401 [*] `os` imported but unused
|
||||
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes_syntax_errors() {
|
||||
let (mut env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
|
||||
syntax_errors.py:3:12: SyntaxError: Expected ')', found newline
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
syntax_errors.py:1:15: error[invalid-syntax] SyntaxError: Expected one or more symbol names after import
|
||||
syntax_errors.py:3:12: error[invalid-syntax] SyntaxError: Expected ')', found newline
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_output() {
|
||||
let (env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
notebook.ipynb:cell 1:2:8: error[unused-import] `os` imported but unused
|
||||
notebook.ipynb:cell 2:2:8: error[unused-import] `math` imported but unused
|
||||
notebook.ipynb:cell 3:4:5: error[unused-variable] Local variable `x` is assigned to but never used
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.format(DiagnosticFormat::Concise);
|
||||
|
||||
let diag = env.err().build();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@"error[test-diagnostic] main diagnostic message",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
#[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::Full);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r#"
|
||||
error[unused-import]: `os` imported but unused
|
||||
--> fib.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
|
||||
error[unused-variable]: Local variable `x` is assigned to but never used
|
||||
--> fib.py:6:5
|
||||
|
|
||||
4 | def fibonacci(n):
|
||||
5 | """Compute the nth number in the Fibonacci sequence."""
|
||||
6 | x = 1
|
||||
| ^
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
help: Remove assignment to unused variable `x`
|
||||
|
||||
error[undefined-name]: Undefined name `a`
|
||||
--> undef.py:1:4
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
error[invalid-syntax]: SyntaxError: Expected one or more symbol names after import
|
||||
--> syntax_errors.py:1:15
|
||||
|
|
||||
1 | from os import
|
||||
| ^
|
||||
2 |
|
||||
3 | if call(foo
|
||||
|
|
||||
|
||||
error[invalid-syntax]: SyntaxError: Expected ')', found newline
|
||||
--> syntax_errors.py:3:12
|
||||
|
|
||||
1 | from os import
|
||||
2 |
|
||||
3 | if call(foo
|
||||
| ^
|
||||
4 | def bar():
|
||||
5 | pass
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ pub(super) fn diagnostic_to_json<'a>(
|
||||
|
||||
let fix = diagnostic.fix().map(|fix| JsonFix {
|
||||
applicability: fix.applicability(),
|
||||
message: diagnostic.first_help_text(),
|
||||
message: diagnostic.suggestion(),
|
||||
edits: ExpandedEdits {
|
||||
edits: fix.edits(),
|
||||
notebook_index,
|
||||
|
||||
@@ -41,8 +41,6 @@ pub struct DiagnosticStylesheet {
|
||||
pub(crate) line_no: Style,
|
||||
pub(crate) emphasis: Style,
|
||||
pub(crate) none: Style,
|
||||
pub(crate) separator: Style,
|
||||
pub(crate) secondary_code: Style,
|
||||
}
|
||||
|
||||
impl Default for DiagnosticStylesheet {
|
||||
@@ -64,8 +62,6 @@ impl DiagnosticStylesheet {
|
||||
line_no: bright_blue.effects(Effects::BOLD),
|
||||
emphasis: Style::new().effects(Effects::BOLD),
|
||||
none: Style::new(),
|
||||
separator: AnsiColor::Cyan.on_default(),
|
||||
secondary_code: AnsiColor::Red.on_default().effects(Effects::BOLD),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +75,6 @@ impl DiagnosticStylesheet {
|
||||
line_no: Style::new(),
|
||||
emphasis: Style::new(),
|
||||
none: Style::new(),
|
||||
separator: Style::new(),
|
||||
secondary_code: Style::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
use dashmap::mapref::entry::Entry;
|
||||
pub use file_root::{FileRoot, FileRootKind};
|
||||
pub use path::FilePath;
|
||||
@@ -311,6 +312,11 @@ pub struct File {
|
||||
/// the file has been deleted is to change the status to `Deleted`.
|
||||
#[default]
|
||||
status: FileStatus,
|
||||
|
||||
/// Counter that counts the number of created file instances and active file instances.
|
||||
/// Only enabled in debug builds.
|
||||
#[default]
|
||||
count: Count<File>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_source_file::LineIndex;
|
||||
@@ -36,7 +38,11 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
};
|
||||
|
||||
SourceText {
|
||||
inner: Arc::new(SourceTextInner { kind, read_error }),
|
||||
inner: Arc::new(SourceTextInner {
|
||||
kind,
|
||||
read_error,
|
||||
count: Count::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +125,8 @@ impl std::fmt::Debug for SourceText {
|
||||
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize)]
|
||||
struct SourceTextInner {
|
||||
#[get_size(ignore)]
|
||||
count: Count<SourceText>,
|
||||
kind: SourceTextKind,
|
||||
read_error: Option<SourceTextError>,
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ impl<'a> Resolver<'a> {
|
||||
match import {
|
||||
CollectedImport::Import(import) => {
|
||||
let module = resolve_module(self.db, &import)?;
|
||||
Some(module.file(self.db)?.path(self.db))
|
||||
Some(module.file()?.path(self.db))
|
||||
}
|
||||
CollectedImport::ImportFrom(import) => {
|
||||
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
|
||||
@@ -32,7 +32,7 @@ impl<'a> Resolver<'a> {
|
||||
resolve_module(self.db, &parent?)
|
||||
})?;
|
||||
|
||||
Some(module.file(self.db)?.path(self.db))
|
||||
Some(module.file()?.path(self.db))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -142,7 +142,3 @@ field47: typing.Optional[int] | typing.Optional[dict]
|
||||
# avoid reporting twice
|
||||
field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
field49: typing.Optional[complex | complex] | complex
|
||||
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
# Should throw duplicate union member but not fix
|
||||
isinstance(None, typing.Union[None, None])
|
||||
@@ -47,19 +47,3 @@ def _():
|
||||
from builtin import open
|
||||
|
||||
with open(p) as _: ... # No error
|
||||
|
||||
file = "file_1.py"
|
||||
|
||||
rename(file, "file_2.py")
|
||||
|
||||
rename(
|
||||
# commment 1
|
||||
file, # comment 2
|
||||
"file_2.py"
|
||||
,
|
||||
# comment 3
|
||||
)
|
||||
|
||||
rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
|
||||
rename(file, "file_2.py", src_dir_fd=1)
|
||||
@@ -84,25 +84,3 @@ class MyRequestHandler(BaseHTTPRequestHandler):
|
||||
def dont_GET(self):
|
||||
pass
|
||||
|
||||
|
||||
from http.server import CGIHTTPRequestHandler
|
||||
|
||||
|
||||
class MyCGIRequestHandler(CGIHTTPRequestHandler):
|
||||
def do_OPTIONS(self):
|
||||
pass
|
||||
|
||||
def dont_OPTIONS(self):
|
||||
pass
|
||||
|
||||
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
|
||||
|
||||
class MySimpleRequestHandler(SimpleHTTPRequestHandler):
|
||||
def do_OPTIONS(self):
|
||||
pass
|
||||
|
||||
def dont_OPTIONS(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -278,15 +278,3 @@ def f():
|
||||
for i in src:
|
||||
if lambda: 0:
|
||||
dst.append(i)
|
||||
|
||||
def f():
|
||||
i = "xyz"
|
||||
result = []
|
||||
for i in range(3):
|
||||
result.append(x for x in [i])
|
||||
|
||||
def f():
|
||||
i = "xyz"
|
||||
result = []
|
||||
for i in range(3):
|
||||
result.append((x for x in [i]))
|
||||
@@ -1,5 +0,0 @@
|
||||
#
|
||||
x = 0 \
|
||||
#
|
||||
+1
|
||||
print(x)
|
||||
@@ -79,8 +79,3 @@ 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}')
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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)
|
||||
@@ -1039,10 +1039,14 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
|
||||
}
|
||||
if checker.any_rule_enabled(&[
|
||||
Rule::OsChmod,
|
||||
Rule::OsMkdir,
|
||||
Rule::OsMakedirs,
|
||||
Rule::OsRename,
|
||||
Rule::OsReplace,
|
||||
Rule::OsStat,
|
||||
Rule::OsPathJoin,
|
||||
Rule::OsPathSamefile,
|
||||
Rule::OsPathSplitext,
|
||||
Rule::BuiltinOpen,
|
||||
Rule::PyPath,
|
||||
@@ -1108,18 +1112,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::OsGetcwd) {
|
||||
flake8_use_pathlib::rules::os_getcwd(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsChmod) {
|
||||
flake8_use_pathlib::rules::os_chmod(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsRename) {
|
||||
flake8_use_pathlib::rules::os_rename(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsReplace) {
|
||||
flake8_use_pathlib::rules::os_replace(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsPathSamefile) {
|
||||
flake8_use_pathlib::rules::os_path_samefile(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
|
||||
flake8_use_pathlib::rules::path_constructor_current_directory(
|
||||
checker, call, segments,
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(crate) fn check_tokens(
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::EmptyComment) {
|
||||
pylint::rules::empty_comments(context, comment_ranges, locator, indexer);
|
||||
pylint::rules::empty_comments(context, comment_ranges, locator);
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::AmbiguousUnicodeCharacterComment) {
|
||||
|
||||
@@ -920,11 +920,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
|
||||
// flake8-use-pathlib
|
||||
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
|
||||
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsChmod),
|
||||
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsChmod),
|
||||
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
|
||||
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
|
||||
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRename),
|
||||
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReplace),
|
||||
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename),
|
||||
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace),
|
||||
(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),
|
||||
@@ -940,7 +940,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin),
|
||||
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename),
|
||||
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
|
||||
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile),
|
||||
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile),
|
||||
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
|
||||
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),
|
||||
(Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath),
|
||||
|
||||
@@ -75,12 +75,11 @@ where
|
||||
);
|
||||
|
||||
let span = Span::from(file).with_range(range);
|
||||
let annotation = Annotation::primary(span);
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
let mut annotation = Annotation::primary(span);
|
||||
if let Some(suggestion) = suggestion {
|
||||
diagnostic.help(suggestion);
|
||||
annotation = annotation.message(suggestion);
|
||||
}
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
if let Some(fix) = fix {
|
||||
diagnostic.set_fix(fix);
|
||||
|
||||
@@ -6,12 +6,13 @@ use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use ruff_annotate_snippets::{Level, Renderer, Snippet};
|
||||
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode};
|
||||
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_source_file::{LineColumn, OneIndexed};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::fs::relativize_path;
|
||||
use crate::line_width::{IndentWidth, LineWidthBuilder};
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
@@ -20,6 +21,8 @@ use crate::settings::types::UnsafeFixes;
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
struct EmitterFlags: u8 {
|
||||
/// Whether to show the fix status of a diagnostic.
|
||||
const SHOW_FIX_STATUS = 1 << 0;
|
||||
/// Whether to show the diff of a fix, for diagnostics that have a fix.
|
||||
const SHOW_FIX_DIFF = 1 << 1;
|
||||
/// Whether to show the source code of a diagnostic.
|
||||
@@ -27,27 +30,17 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextEmitter {
|
||||
flags: EmitterFlags,
|
||||
config: DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl Default for TextEmitter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
flags: EmitterFlags::default(),
|
||||
config: DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Concise)
|
||||
.hide_severity(true)
|
||||
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()),
|
||||
}
|
||||
}
|
||||
unsafe_fixes: UnsafeFixes,
|
||||
}
|
||||
|
||||
impl TextEmitter {
|
||||
#[must_use]
|
||||
pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self {
|
||||
self.config = self.config.show_fix_status(show_fix_status);
|
||||
self.flags
|
||||
.set(EmitterFlags::SHOW_FIX_STATUS, show_fix_status);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -65,15 +58,7 @@ impl TextEmitter {
|
||||
|
||||
#[must_use]
|
||||
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
|
||||
self.config = self
|
||||
.config
|
||||
.fix_applicability(unsafe_fixes.required_applicability());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_preview(mut self, preview: bool) -> Self {
|
||||
self.config = self.config.preview(preview);
|
||||
self.unsafe_fixes = unsafe_fixes;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -86,10 +71,51 @@ impl Emitter for TextEmitter {
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in diagnostics {
|
||||
write!(writer, "{}", message.display(context, &self.config))?;
|
||||
|
||||
let filename = message.expect_ruff_filename();
|
||||
write!(
|
||||
writer,
|
||||
"{path}{sep}",
|
||||
path = relativize_path(&filename).bold(),
|
||||
sep = ":".cyan(),
|
||||
)?;
|
||||
|
||||
let start_location = message.expect_ruff_start_location();
|
||||
let notebook_index = context.notebook_index(&filename);
|
||||
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let diagnostic_location = if let Some(notebook_index) = notebook_index {
|
||||
write!(
|
||||
writer,
|
||||
"cell {cell}{sep}",
|
||||
cell = notebook_index
|
||||
.cell(start_location.line)
|
||||
.unwrap_or(OneIndexed::MIN),
|
||||
sep = ":".cyan(),
|
||||
)?;
|
||||
|
||||
LineColumn {
|
||||
line: notebook_index
|
||||
.cell_row(start_location.line)
|
||||
.unwrap_or(OneIndexed::MIN),
|
||||
column: start_location.column,
|
||||
}
|
||||
} else {
|
||||
start_location
|
||||
};
|
||||
|
||||
writeln!(
|
||||
writer,
|
||||
"{row}{sep}{col}{sep} {code_and_body}",
|
||||
row = diagnostic_location.line,
|
||||
col = diagnostic_location.column,
|
||||
sep = ":".cyan(),
|
||||
code_and_body = RuleCodeAndBody {
|
||||
message,
|
||||
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS),
|
||||
unsafe_fixes: self.unsafe_fixes,
|
||||
}
|
||||
)?;
|
||||
|
||||
if self.flags.intersects(EmitterFlags::SHOW_SOURCE) {
|
||||
// The `0..0` range is used to highlight file-level diagnostics.
|
||||
if message.expect_range() != TextRange::default() {
|
||||
@@ -160,7 +186,7 @@ pub(super) struct MessageCodeFrame<'a> {
|
||||
|
||||
impl Display for MessageCodeFrame<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let suggestion = self.message.first_help_text();
|
||||
let suggestion = self.message.suggestion();
|
||||
let footers = if let Some(suggestion) = suggestion {
|
||||
vec![Level::Help.title(suggestion)]
|
||||
} else {
|
||||
|
||||
@@ -134,26 +134,6 @@ pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) ->
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_chmod_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_rename_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_replace_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_path_samefile_enabled(settings: &LinterSettings) -> bool {
|
||||
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()
|
||||
|
||||
@@ -64,7 +64,6 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let mut union_type = UnionKind::TypingUnion;
|
||||
let mut optional_present = false;
|
||||
// Adds a member to `literal_exprs` if it is a `Literal` annotation
|
||||
let mut check_for_duplicate_members = |expr: &'a Expr, parent: &'a Expr| {
|
||||
if matches!(parent, Expr::BinOp(_)) {
|
||||
@@ -75,7 +74,6 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
&& is_optional_type(checker, expr)
|
||||
{
|
||||
// If the union member is an `Optional`, add a virtual `None` literal.
|
||||
optional_present = true;
|
||||
&VIRTUAL_NONE_LITERAL
|
||||
} else {
|
||||
expr
|
||||
@@ -89,7 +87,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
DuplicateUnionMember {
|
||||
duplicate_name: checker.generator().expr(virtual_expr),
|
||||
},
|
||||
// Use the real expression's range for diagnostics.
|
||||
// Use the real expression's range for diagnostics,
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
@@ -106,13 +104,6 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not reduce `Union[None, ... None]` to avoid introducing a `TypeError` unintentionally
|
||||
// e.g. `isinstance(None, Union[None, None])`, if reduced to `isinstance(None, None)`, causes
|
||||
// `TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union` to throw.
|
||||
if unique_nodes.iter().all(|expr| expr.is_none_literal_expr()) && !optional_present {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark [`Fix`] as unsafe when comments are in range.
|
||||
let applicability = if checker.comment_ranges().intersects(expr.range()) {
|
||||
Applicability::Unsafe
|
||||
|
||||
@@ -974,8 +974,6 @@ PYI016.py:143:61: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[typing.Optional[complex], complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -983,8 +981,6 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -994,15 +990,3 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: typing.Optional[complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:148:37: PYI016 Duplicate union member `None`
|
||||
|
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 | # Should throw duplicate union member but not fix
|
||||
148 | isinstance(None, typing.Union[None, None])
|
||||
| ^^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `None`
|
||||
|
||||
@@ -1162,8 +1162,6 @@ PYI016.py:143:61: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[None, complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:143:72: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1181,8 +1179,6 @@ PYI016.py:143:72: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[None, complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1190,8 +1186,6 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -1201,9 +1195,6 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: None | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1211,8 +1202,6 @@ PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -1222,15 +1211,3 @@ PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: None | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:148:37: PYI016 Duplicate union member `None`
|
||||
|
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 | # Should throw duplicate union member but not fix
|
||||
148 | isinstance(None, typing.Union[None, None])
|
||||
| ^^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `None`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::{Applicability, Edit, Fix, Violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||
use ruff_python_semantic::{SemanticModel, analyze::typing};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_ast::{Expr, ExprCall};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
|
||||
@@ -72,85 +72,3 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
|
||||
match expr {
|
||||
Expr::Name(name) => Some(name),
|
||||
Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
|
||||
pub(crate) fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
if matches!(
|
||||
expr,
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(_),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(name) = get_name_expr(expr) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
typing::is_int(binding, semantic)
|
||||
}
|
||||
|
||||
pub(crate) fn check_os_pathlib_two_arg_calls(
|
||||
checker: &Checker,
|
||||
call: &ExprCall,
|
||||
attr: &str,
|
||||
path_arg: &str,
|
||||
second_arg: &str,
|
||||
fix_enabled: bool,
|
||||
violation: impl Violation,
|
||||
) {
|
||||
let range = call.range();
|
||||
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
|
||||
|
||||
let (Some(path_expr), Some(second_expr)) = (
|
||||
call.arguments.find_argument_value(path_arg, 0),
|
||||
call.arguments.find_argument_value(second_arg, 1),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let path_code = checker.locator().slice(path_expr.range());
|
||||
let second_code = checker.locator().slice(second_expr.range());
|
||||
|
||||
if fix_enabled {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import("pathlib", "Path"),
|
||||
call.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
|
||||
let replacement = if is_pathlib_path_call(checker, path_expr) {
|
||||
format!("{path_code}.{attr}({second_code})")
|
||||
} else {
|
||||
format!("{binding}({path_code}).{attr}({second_code})")
|
||||
};
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
Ok(Fix::applicable_edits(
|
||||
Edit::range_replacement(replacement, range),
|
||||
[import_edit],
|
||||
applicability,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
pub(crate) use glob_rule::*;
|
||||
pub(crate) use invalid_pathlib_with_suffix::*;
|
||||
pub(crate) use os_chmod::*;
|
||||
pub(crate) use os_getcwd::*;
|
||||
pub(crate) use os_path_abspath::*;
|
||||
pub(crate) use os_path_basename::*;
|
||||
@@ -15,11 +14,8 @@ pub(crate) use os_path_isabs::*;
|
||||
pub(crate) use os_path_isdir::*;
|
||||
pub(crate) use os_path_isfile::*;
|
||||
pub(crate) use os_path_islink::*;
|
||||
pub(crate) use os_path_samefile::*;
|
||||
pub(crate) use os_readlink::*;
|
||||
pub(crate) use os_remove::*;
|
||||
pub(crate) use os_rename::*;
|
||||
pub(crate) use os_replace::*;
|
||||
pub(crate) use os_rmdir::*;
|
||||
pub(crate) use os_sep_split::*;
|
||||
pub(crate) use os_unlink::*;
|
||||
@@ -28,7 +24,6 @@ pub(crate) use replaceable_by_pathlib::*;
|
||||
|
||||
mod glob_rule;
|
||||
mod invalid_pathlib_with_suffix;
|
||||
mod os_chmod;
|
||||
mod os_getcwd;
|
||||
mod os_path_abspath;
|
||||
mod os_path_basename;
|
||||
@@ -43,11 +38,8 @@ mod os_path_isabs;
|
||||
mod os_path_isdir;
|
||||
mod os_path_isfile;
|
||||
mod os_path_islink;
|
||||
mod os_path_samefile;
|
||||
mod os_readlink;
|
||||
mod os_remove;
|
||||
mod os_rename;
|
||||
mod os_replace;
|
||||
mod os_rmdir;
|
||||
mod os_sep_split;
|
||||
mod os_unlink;
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_chmod_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, is_file_descriptor, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.chmod`.
|
||||
///
|
||||
/// ## 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.chmod()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.chmod()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.chmod("file.py", 0o444)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("file.py").chmod(0o444)
|
||||
/// ```
|
||||
///
|
||||
/// ## 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.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
|
||||
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
|
||||
/// - [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 OsChmod;
|
||||
|
||||
impl Violation for OsChmod {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.chmod()` should be replaced by `Path.chmod()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path(...).chmod(...)`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH101
|
||||
pub(crate) fn os_chmod(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if segments != ["os", "chmod"] {
|
||||
return;
|
||||
}
|
||||
|
||||
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
|
||||
// ```
|
||||
if call
|
||||
.arguments
|
||||
.find_argument_value("path", 0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"chmod",
|
||||
"path",
|
||||
"mode",
|
||||
is_fix_os_chmod_enabled(checker.settings()),
|
||||
OsChmod,
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_path_samefile_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_two_arg_calls;
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.samefile`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os.path`. When possible, using `Path` object
|
||||
/// methods such as `Path.samefile()` can improve readability over the `os.path`
|
||||
/// module's counterparts (e.g., `os.path.samefile()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.path.samefile("f1.py", "f2.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("f1.py").samefile("f2.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## 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.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
|
||||
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
|
||||
/// - [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 OsPathSamefile;
|
||||
|
||||
impl Violation for OsPathSamefile {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.path.samefile()` should be replaced by `Path.samefile()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path(...).samefile()`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH121
|
||||
pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if segments != ["os", "path", "samefile"] {
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"samefile",
|
||||
"f1",
|
||||
"f2",
|
||||
is_fix_os_path_samefile_enabled(checker.settings()),
|
||||
OsPathSamefile,
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_rename_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.rename`.
|
||||
///
|
||||
/// ## 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.rename()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.rename()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.rename("old.py", "new.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("old.py").rename("new.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## 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.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
|
||||
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
|
||||
/// - [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 OsRename;
|
||||
|
||||
impl Violation for OsRename {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.rename()` should be replaced by `Path.rename()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path(...).rename(...)`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH104
|
||||
pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if segments != ["os", "rename"] {
|
||||
return;
|
||||
}
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
|
||||
// ```
|
||||
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"rename",
|
||||
"src",
|
||||
"dst",
|
||||
is_fix_os_rename_enabled(checker.settings()),
|
||||
OsRename,
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_replace_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.replace`.
|
||||
///
|
||||
/// ## 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.replace()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.replace()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.replace("old.py", "new.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("old.py").replace("new.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## 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.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
|
||||
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
|
||||
/// - [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 OsReplace;
|
||||
|
||||
impl Violation for OsReplace {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.replace()` should be replaced by `Path.replace()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path(...).replace(...)`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH105
|
||||
pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if segments != ["os", "replace"] {
|
||||
return;
|
||||
}
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
|
||||
// ```
|
||||
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"replace",
|
||||
"src",
|
||||
"dst",
|
||||
is_fix_os_replace_enabled(checker.settings()),
|
||||
OsReplace,
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
is_file_descriptor, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::rules::flake8_use_pathlib::{
|
||||
rules::Glob,
|
||||
violations::{
|
||||
BuiltinOpen, Joiner, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSplitext, OsStat,
|
||||
OsSymlink, PyPath,
|
||||
},
|
||||
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, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSamefile,
|
||||
OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath,
|
||||
};
|
||||
|
||||
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
@@ -20,6 +18,24 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
|
||||
let range = call.func.range();
|
||||
match qualified_name.segments() {
|
||||
// PTH101
|
||||
["os", "chmod"] => {
|
||||
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
|
||||
// ```
|
||||
if call
|
||||
.arguments
|
||||
.find_argument_value("path", 0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsChmod, range)
|
||||
}
|
||||
// PTH102
|
||||
["os", "makedirs"] => checker.report_diagnostic_if_enabled(OsMakedirs, range),
|
||||
// PTH103
|
||||
@@ -35,6 +51,38 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsMkdir, range)
|
||||
}
|
||||
// PTH104
|
||||
["os", "rename"] => {
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
|
||||
// ```
|
||||
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsRename, range)
|
||||
}
|
||||
// PTH105
|
||||
["os", "replace"] => {
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
|
||||
// ```
|
||||
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsReplace, range)
|
||||
}
|
||||
// PTH116
|
||||
["os", "stat"] => {
|
||||
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
|
||||
@@ -76,6 +124,8 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
},
|
||||
range,
|
||||
),
|
||||
// PTH121
|
||||
["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range),
|
||||
// PTH122
|
||||
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
|
||||
// PTH211
|
||||
@@ -184,6 +234,37 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
|
||||
fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
if matches!(
|
||||
expr,
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(_),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(name) = get_name_expr(expr) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
typing::is_int(binding, semantic)
|
||||
}
|
||||
|
||||
fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
|
||||
match expr {
|
||||
Expr::Name(name) => Some(name),
|
||||
Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if argument `name` is set to a non-default `None` value.
|
||||
fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usize) -> bool {
|
||||
arguments
|
||||
|
||||
@@ -20,7 +20,6 @@ full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
9 | aaa = os.mkdir(p)
|
||||
10 | os.makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -51,7 +50,6 @@ full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
12 | os.replace(p)
|
||||
13 | os.rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -62,7 +60,6 @@ full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
13 | os.rmdir(p)
|
||||
14 | os.remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
full_name.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -256,7 +253,6 @@ full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
|
||||
31 | os.path.splitext(p)
|
||||
32 | with open(p) as fp:
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -20,7 +20,6 @@ import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
9 | aaa = foo.mkdir(p)
|
||||
10 | foo.makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -51,7 +50,6 @@ import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
12 | foo.replace(p)
|
||||
13 | foo.rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -62,7 +60,6 @@ import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
13 | foo.rmdir(p)
|
||||
14 | foo.remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_as.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -255,7 +252,6 @@ import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
|
||||
| ^^^^^^^^^^^^^^ PTH121
|
||||
31 | foo_p.splitext(p)
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -20,7 +20,6 @@ import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
11 | aaa = mkdir(p)
|
||||
12 | makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -51,7 +50,6 @@ import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
14 | replace(p)
|
||||
15 | rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -62,7 +60,6 @@ import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()
|
||||
15 | rmdir(p)
|
||||
16 | remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_from.py:15:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -256,7 +253,6 @@ import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.sam
|
||||
33 | splitext(p)
|
||||
34 | with open(p) as fp:
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
@@ -293,36 +289,3 @@ import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()`
|
||||
43 | with open(p) as _: ... # Error
|
||||
| ^^^^ PTH123
|
||||
|
|
||||
|
||||
import_from.py:53:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
51 | file = "file_1.py"
|
||||
52 |
|
||||
53 | rename(file, "file_2.py")
|
||||
| ^^^^^^ PTH104
|
||||
54 |
|
||||
55 | rename(
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from.py:55:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
53 | rename(file, "file_2.py")
|
||||
54 |
|
||||
55 | rename(
|
||||
| ^^^^^^ PTH104
|
||||
56 | # commment 1
|
||||
57 | file, # comment 2
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from.py:63:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
61 | )
|
||||
62 |
|
||||
63 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
| ^^^^^^ PTH104
|
||||
64 |
|
||||
65 | rename(file, "file_2.py", src_dir_fd=1)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
@@ -20,7 +20,6 @@ import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
16 | aaa = xmkdir(p)
|
||||
17 | xmakedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -51,7 +50,6 @@ import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename(
|
||||
19 | xreplace(p)
|
||||
20 | xrmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -62,7 +60,6 @@ import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replac
|
||||
20 | xrmdir(p)
|
||||
21 | xremove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_from_as.py:20:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -255,7 +252,6 @@ import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.
|
||||
| ^^^^^^^^^ PTH121
|
||||
38 | xsplitext(p)
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -34,7 +34,6 @@ full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
9 | aaa = os.mkdir(p)
|
||||
10 | os.makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -65,7 +64,6 @@ full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
12 | os.replace(p)
|
||||
13 | os.rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -76,7 +74,6 @@ full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
13 | os.rmdir(p)
|
||||
14 | os.remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
full_name.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -474,7 +471,6 @@ full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
|
||||
31 | os.path.splitext(p)
|
||||
32 | with open(p) as fp:
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -34,7 +34,6 @@ import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
9 | aaa = foo.mkdir(p)
|
||||
10 | foo.makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -65,7 +64,6 @@ import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
12 | foo.replace(p)
|
||||
13 | foo.rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -76,7 +74,6 @@ import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
13 | foo.rmdir(p)
|
||||
14 | foo.remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_as.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -472,7 +469,6 @@ import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
|
||||
| ^^^^^^^^^^^^^^ PTH121
|
||||
31 | foo_p.splitext(p)
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -35,7 +35,6 @@ import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
11 | aaa = mkdir(p)
|
||||
12 | makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -66,7 +65,6 @@ import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
14 | replace(p)
|
||||
15 | rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -77,7 +75,6 @@ import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()
|
||||
15 | rmdir(p)
|
||||
16 | remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_from.py:15:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -487,7 +484,6 @@ import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.sam
|
||||
33 | splitext(p)
|
||||
34 | with open(p) as fp:
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
@@ -524,95 +520,3 @@ import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()`
|
||||
43 | with open(p) as _: ... # Error
|
||||
| ^^^^ PTH123
|
||||
|
|
||||
|
||||
import_from.py:53:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
51 | file = "file_1.py"
|
||||
52 |
|
||||
53 | rename(file, "file_2.py")
|
||||
| ^^^^^^ PTH104
|
||||
54 |
|
||||
55 | rename(
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | from os import remove, unlink, getcwd, readlink, stat
|
||||
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
|
||||
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
|
||||
5 |+import pathlib
|
||||
5 6 |
|
||||
6 7 | p = "/foo"
|
||||
7 8 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
50 51 |
|
||||
51 52 | file = "file_1.py"
|
||||
52 53 |
|
||||
53 |-rename(file, "file_2.py")
|
||||
54 |+pathlib.Path(file).rename("file_2.py")
|
||||
54 55 |
|
||||
55 56 | rename(
|
||||
56 57 | # commment 1
|
||||
|
||||
import_from.py:55:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
53 | rename(file, "file_2.py")
|
||||
54 |
|
||||
55 | rename(
|
||||
| ^^^^^^ PTH104
|
||||
56 | # commment 1
|
||||
57 | file, # comment 2
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | from os import remove, unlink, getcwd, readlink, stat
|
||||
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
|
||||
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
|
||||
5 |+import pathlib
|
||||
5 6 |
|
||||
6 7 | p = "/foo"
|
||||
7 8 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
52 53 |
|
||||
53 54 | rename(file, "file_2.py")
|
||||
54 55 |
|
||||
55 |-rename(
|
||||
56 |- # commment 1
|
||||
57 |- file, # comment 2
|
||||
58 |- "file_2.py"
|
||||
59 |- ,
|
||||
60 |- # comment 3
|
||||
61 |-)
|
||||
56 |+pathlib.Path(file).rename("file_2.py")
|
||||
62 57 |
|
||||
63 58 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
64 59 |
|
||||
|
||||
import_from.py:63:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
61 | )
|
||||
62 |
|
||||
63 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
| ^^^^^^ PTH104
|
||||
64 |
|
||||
65 | rename(file, "file_2.py", src_dir_fd=1)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | from os import remove, unlink, getcwd, readlink, stat
|
||||
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
|
||||
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
|
||||
5 |+import pathlib
|
||||
5 6 |
|
||||
6 7 | p = "/foo"
|
||||
7 8 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
60 61 | # comment 3
|
||||
61 62 | )
|
||||
62 63 |
|
||||
63 |-rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
64 |+pathlib.Path(file).rename("file_2.py")
|
||||
64 65 |
|
||||
65 66 | rename(file, "file_2.py", src_dir_fd=1)
|
||||
|
||||
@@ -35,7 +35,6 @@ import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
16 | aaa = xmkdir(p)
|
||||
17 | xmakedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -66,7 +65,6 @@ import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename(
|
||||
19 | xreplace(p)
|
||||
20 | xrmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -77,7 +75,6 @@ import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replac
|
||||
20 | xrmdir(p)
|
||||
21 | xremove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_from_as.py:20:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -485,7 +482,6 @@ import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.
|
||||
| ^^^^^^^^^ PTH121
|
||||
38 | xsplitext(p)
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -2,6 +2,51 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
|
||||
use crate::Violation;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.chmod`.
|
||||
///
|
||||
/// ## 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.chmod()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.chmod()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.chmod("file.py", 0o444)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("file.py").chmod(0o444)
|
||||
/// ```
|
||||
///
|
||||
/// ## 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.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
|
||||
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
|
||||
/// - [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 OsChmod;
|
||||
|
||||
impl Violation for OsChmod {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.chmod()` should be replaced by `Path.chmod()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.makedirs`.
|
||||
///
|
||||
@@ -92,6 +137,99 @@ impl Violation for OsMkdir {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.rename`.
|
||||
///
|
||||
/// ## 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.rename()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.rename()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.rename("old.py", "new.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("old.py").rename("new.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## 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.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
|
||||
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
|
||||
/// - [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 OsRename;
|
||||
|
||||
impl Violation for OsRename {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.rename()` should be replaced by `Path.rename()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.replace`.
|
||||
///
|
||||
/// ## 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.replace()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.replace()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.replace("old.py", "new.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("old.py").replace("new.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## 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.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
|
||||
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
|
||||
/// - [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 OsReplace;
|
||||
|
||||
impl Violation for OsReplace {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.replace()` should be replaced by `Path.replace()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.stat`.
|
||||
///
|
||||
@@ -209,6 +347,51 @@ pub(crate) enum Joiner {
|
||||
Joinpath,
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.samefile`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os.path`. When possible, using `Path` object
|
||||
/// methods such as `Path.samefile()` can improve readability over the `os.path`
|
||||
/// module's counterparts (e.g., `os.path.samefile()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.path.samefile("f1.py", "f2.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("f1.py").samefile("f2.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## 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.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
|
||||
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
|
||||
/// - [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 OsPathSamefile;
|
||||
|
||||
impl Violation for OsPathSamefile {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.path.samefile()` should be replaced by `Path.samefile()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.splitext`.
|
||||
///
|
||||
|
||||
@@ -100,7 +100,7 @@ pub(crate) fn invalid_function_name(
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore the do_* methods of the http.server.BaseHTTPRequestHandler class and its subclasses
|
||||
// Ignore the do_* methods of the http.server.BaseHTTPRequestHandler class
|
||||
if name.starts_with("do_")
|
||||
&& parent_class.is_some_and(|class| {
|
||||
any_base_class(class, semantic, &mut |superclass| {
|
||||
@@ -108,13 +108,7 @@ pub(crate) fn invalid_function_name(
|
||||
qualified.is_some_and(|name| {
|
||||
matches!(
|
||||
name.segments(),
|
||||
[
|
||||
"http",
|
||||
"server",
|
||||
"BaseHTTPRequestHandler"
|
||||
| "CGIHTTPRequestHandler"
|
||||
| "SimpleHTTPRequestHandler"
|
||||
]
|
||||
["http", "server", "BaseHTTPRequestHandler"]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,21 +55,3 @@ N802.py:84:9: N802 Function name `dont_GET` should be lowercase
|
||||
| ^^^^^^^^ N802
|
||||
85 | pass
|
||||
|
|
||||
|
||||
N802.py:95:9: N802 Function name `dont_OPTIONS` should be lowercase
|
||||
|
|
||||
93 | pass
|
||||
94 |
|
||||
95 | def dont_OPTIONS(self):
|
||||
| ^^^^^^^^^^^^ N802
|
||||
96 | pass
|
||||
|
|
||||
|
||||
N802.py:106:9: N802 Function name `dont_OPTIONS` should be lowercase
|
||||
|
|
||||
104 | pass
|
||||
105 |
|
||||
106 | def dont_OPTIONS(self):
|
||||
| ^^^^^^^^^^^^ N802
|
||||
107 | pass
|
||||
|
|
||||
|
||||
@@ -406,14 +406,7 @@ fn convert_to_list_extend(
|
||||
};
|
||||
let target_str = locator.slice(for_stmt.target.range());
|
||||
let elt_str = locator.slice(to_append);
|
||||
let generator_str = if to_append
|
||||
.as_generator_expr()
|
||||
.is_some_and(|generator| !generator.parenthesized)
|
||||
{
|
||||
format!("({elt_str}) {for_type} {target_str} in {for_iter_str}{if_str}")
|
||||
} else {
|
||||
format!("{elt_str} {for_type} {target_str} in {for_iter_str}{if_str}")
|
||||
};
|
||||
let generator_str = format!("{elt_str} {for_type} {target_str} in {for_iter_str}{if_str}");
|
||||
|
||||
let variable_name = locator.slice(binding);
|
||||
let for_loop_inline_comments = comment_strings_in_range(
|
||||
|
||||
@@ -241,27 +241,5 @@ PERF401.py:280:13: PERF401 Use `list.extend` to create a transformed list
|
||||
279 | if lambda: 0:
|
||||
280 | dst.append(i)
|
||||
| ^^^^^^^^^^^^^ PERF401
|
||||
281 |
|
||||
282 | def f():
|
||||
|
|
||||
= help: Replace for loop with list.extend
|
||||
|
||||
PERF401.py:286:9: PERF401 Use a list comprehension to create a transformed list
|
||||
|
|
||||
284 | result = []
|
||||
285 | for i in range(3):
|
||||
286 | result.append(x for x in [i])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
287 |
|
||||
288 | def f():
|
||||
|
|
||||
= help: Replace for loop with list comprehension
|
||||
|
||||
PERF401.py:292:9: PERF401 Use a list comprehension to create a transformed list
|
||||
|
|
||||
290 | result = []
|
||||
291 | for i in range(3):
|
||||
292 | result.append((x for x in [i]))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
|
|
||||
= help: Replace for loop with list comprehension
|
||||
|
||||
@@ -566,8 +566,6 @@ PERF401.py:280:13: PERF401 [*] Use `list.extend` to create a transformed list
|
||||
279 | if lambda: 0:
|
||||
280 | dst.append(i)
|
||||
| ^^^^^^^^^^^^^ PERF401
|
||||
281 |
|
||||
282 | def f():
|
||||
|
|
||||
= help: Replace for loop with list.extend
|
||||
|
||||
@@ -579,47 +577,3 @@ PERF401.py:280:13: PERF401 [*] Use `list.extend` to create a transformed list
|
||||
279 |- if lambda: 0:
|
||||
280 |- dst.append(i)
|
||||
278 |+ dst.extend(i for i in src if (lambda: 0))
|
||||
281 279 |
|
||||
282 280 | def f():
|
||||
283 281 | i = "xyz"
|
||||
|
||||
PERF401.py:286:9: PERF401 [*] Use a list comprehension to create a transformed list
|
||||
|
|
||||
284 | result = []
|
||||
285 | for i in range(3):
|
||||
286 | result.append(x for x in [i])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
287 |
|
||||
288 | def f():
|
||||
|
|
||||
= help: Replace for loop with list comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
281 281 |
|
||||
282 282 | def f():
|
||||
283 283 | i = "xyz"
|
||||
284 |- result = []
|
||||
285 |- for i in range(3):
|
||||
286 |- result.append(x for x in [i])
|
||||
284 |+ result = [(x for x in [i]) for i in range(3)]
|
||||
287 285 |
|
||||
288 286 | def f():
|
||||
289 287 | i = "xyz"
|
||||
|
||||
PERF401.py:292:9: PERF401 [*] Use a list comprehension to create a transformed list
|
||||
|
|
||||
290 | result = []
|
||||
291 | for i in range(3):
|
||||
292 | result.append((x for x in [i]))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
|
|
||||
= help: Replace for loop with list comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
287 287 |
|
||||
288 288 | def f():
|
||||
289 289 | i = "xyz"
|
||||
290 |- result = []
|
||||
291 |- for i in range(3):
|
||||
292 |- result.append((x for x in [i]))
|
||||
290 |+ result = [(x for x in [i]) for i in range(3)]
|
||||
|
||||
@@ -48,7 +48,6 @@ mod tests {
|
||||
#[test_case(Rule::ComparisonWithItself, Path::new("comparison_with_itself.py"))]
|
||||
#[test_case(Rule::EqWithoutHash, Path::new("eq_without_hash.py"))]
|
||||
#[test_case(Rule::EmptyComment, Path::new("empty_comment.py"))]
|
||||
#[test_case(Rule::EmptyComment, Path::new("empty_comment_line_continuation.py"))]
|
||||
#[test_case(Rule::ManualFromImport, Path::new("import_aliasing.py"))]
|
||||
#[test_case(Rule::IfStmtMinMax, Path::new("if_stmt_min_max.py"))]
|
||||
#[test_case(Rule::SingleStringSlots, Path::new("single_string_slots.py"))]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_trivia::{CommentRanges, is_python_whitespace};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
@@ -50,7 +49,6 @@ pub(crate) fn empty_comments(
|
||||
context: &LintContext,
|
||||
comment_ranges: &CommentRanges,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
) {
|
||||
let block_comments = comment_ranges.block_comments(locator.contents());
|
||||
|
||||
@@ -61,12 +59,12 @@ pub(crate) fn empty_comments(
|
||||
}
|
||||
|
||||
// If the line contains an empty comment, add a diagnostic.
|
||||
empty_comment(context, range, locator, indexer);
|
||||
empty_comment(context, range, locator);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [`Diagnostic`] if the comment at the given [`TextRange`] is empty.
|
||||
fn empty_comment(context: &LintContext, range: TextRange, locator: &Locator, indexer: &Indexer) {
|
||||
fn empty_comment(context: &LintContext, range: TextRange, locator: &Locator) {
|
||||
// Check: is the comment empty?
|
||||
if !locator
|
||||
.slice(range)
|
||||
@@ -97,20 +95,12 @@ fn empty_comment(context: &LintContext, range: TextRange, locator: &Locator, ind
|
||||
}
|
||||
});
|
||||
|
||||
// If there is no character preceding the comment, this comment must be on its own physical line.
|
||||
// If there is a line preceding the empty comment's line, check if it ends in a line continuation character. (`\`)
|
||||
let is_on_same_logical_line = indexer
|
||||
.preceded_by_continuations(first_hash_col, locator.contents())
|
||||
.is_some();
|
||||
|
||||
if let Some(mut diagnostic) = context
|
||||
.report_diagnostic_if_enabled(EmptyComment, TextRange::new(first_hash_col, line.end()))
|
||||
{
|
||||
diagnostic.set_fix(Fix::safe_edit(
|
||||
if let Some(deletion_start_col) = deletion_start_col {
|
||||
Edit::deletion(line.start() + deletion_start_col, line.end())
|
||||
} else if is_on_same_logical_line {
|
||||
Edit::deletion(first_hash_col, line.end())
|
||||
} else {
|
||||
Edit::range_deletion(locator.full_line_range(first_hash_col))
|
||||
},
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
empty_comment_line_continuation.py:1:1: PLR2044 [*] Line with empty comment
|
||||
|
|
||||
1 | #
|
||||
| ^ PLR2044
|
||||
2 | x = 0 \
|
||||
3 | #
|
||||
|
|
||||
= help: Delete the empty comment
|
||||
|
||||
ℹ Safe fix
|
||||
1 |-#
|
||||
2 1 | x = 0 \
|
||||
3 2 | #
|
||||
4 3 | +1
|
||||
|
||||
empty_comment_line_continuation.py:3:1: PLR2044 [*] Line with empty comment
|
||||
|
|
||||
1 | #
|
||||
2 | x = 0 \
|
||||
3 | #
|
||||
| ^ PLR2044
|
||||
4 | +1
|
||||
5 | print(x)
|
||||
|
|
||||
= help: Delete the empty comment
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | #
|
||||
2 2 | x = 0 \
|
||||
3 |-#
|
||||
3 |+
|
||||
4 4 | +1
|
||||
5 5 | print(x)
|
||||
@@ -534,7 +534,6 @@ 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, ExprBytesLiteral, ExprCall, ExprCompare, ExprContext,
|
||||
ExprStringLiteral, ExprUnaryOp, Identifier, UnaryOp,
|
||||
Arguments, CmpOp, Expr, ExprAttribute, ExprCall, ExprCompare, ExprContext, ExprStringLiteral,
|
||||
ExprUnaryOp, Identifier, UnaryOp,
|
||||
};
|
||||
use ruff_python_semantic::analyze::typing::find_binding_value;
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
@@ -72,9 +72,6 @@ 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
|
||||
@@ -99,19 +96,16 @@ 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(literal) = resolve_literal(re_func.pattern, semantic) else {
|
||||
let Some(string_lit) = resolve_string_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 = 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)))),
|
||||
};
|
||||
let has_metacharacters = string_lit
|
||||
.value
|
||||
.to_str()
|
||||
.contains(['.', '^', '$', '*', '+', '?', '{', '[', '\\', '|', '(', ')']);
|
||||
|
||||
if has_metacharacters {
|
||||
return;
|
||||
@@ -192,48 +186,28 @@ impl<'a> ReFunc<'a> {
|
||||
// version
|
||||
("sub", 3) => {
|
||||
let repl = call.arguments.find_argument_value("repl", 1)?;
|
||||
let lit = resolve_literal(repl, semantic)?;
|
||||
let lit = resolve_string_literal(repl, semantic)?;
|
||||
let mut fixable = true;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(ReFunc {
|
||||
kind: ReFuncKind::Sub {
|
||||
repl: fixable.then_some(repl),
|
||||
@@ -355,43 +329,6 @@ 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,
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -272,7 +272,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
|
||||
}
|
||||
|
||||
assert!(
|
||||
!(fixable && diagnostic.first_help_text().is_none()),
|
||||
!(fixable && diagnostic.suggestion().is_none()),
|
||||
"Diagnostic emitted by {rule:?} is fixable but \
|
||||
`Violation::fix_title` returns `None`"
|
||||
);
|
||||
|
||||
@@ -235,7 +235,12 @@ impl TraversalSignal {
|
||||
}
|
||||
|
||||
pub fn walk_annotation<'a, V: SourceOrderVisitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
|
||||
visitor.visit_expr(expr);
|
||||
let node = AnyNodeRef::from(expr);
|
||||
if visitor.enter_node(node).is_traverse() {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
|
||||
visitor.leave_node(node);
|
||||
}
|
||||
|
||||
pub fn walk_decorator<'a, V>(visitor: &mut V, decorator: &'a Decorator)
|
||||
|
||||
@@ -1527,7 +1527,7 @@ impl<'src> Parser<'src> {
|
||||
self.bump(kind.start_token());
|
||||
let elements = self.parse_interpolated_string_elements(
|
||||
flags,
|
||||
InterpolatedStringElementsKind::Regular(kind),
|
||||
InterpolatedStringElementsKind::Regular,
|
||||
kind,
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ 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};
|
||||
@@ -800,7 +799,7 @@ impl WithItemKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
enum InterpolatedStringElementsKind {
|
||||
/// The regular f-string elements.
|
||||
///
|
||||
@@ -808,7 +807,7 @@ enum InterpolatedStringElementsKind {
|
||||
/// ```py
|
||||
/// f"hello {x:.2f} world"
|
||||
/// ```
|
||||
Regular(InterpolatedStringKind),
|
||||
Regular,
|
||||
|
||||
/// The f-string elements are part of the format specifier.
|
||||
///
|
||||
@@ -820,13 +819,15 @@ enum InterpolatedStringElementsKind {
|
||||
}
|
||||
|
||||
impl InterpolatedStringElementsKind {
|
||||
const fn list_terminator(self) -> TokenKind {
|
||||
const fn list_terminators(self) -> TokenSet {
|
||||
match self {
|
||||
InterpolatedStringElementsKind::Regular(string_kind) => string_kind.end_token(),
|
||||
InterpolatedStringElementsKind::Regular => {
|
||||
TokenSet::new([TokenKind::FStringEnd, TokenKind::TStringEnd])
|
||||
}
|
||||
// test_ok fstring_format_spec_terminator
|
||||
// f"hello {x:} world"
|
||||
// f"hello {x:.3f} world"
|
||||
InterpolatedStringElementsKind::FormatSpec => TokenKind::Rbrace,
|
||||
InterpolatedStringElementsKind::FormatSpec => TokenSet::new([TokenKind::Rbrace]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1120,7 +1121,7 @@ impl RecoveryContextKind {
|
||||
.then_some(ListTerminatorKind::Regular),
|
||||
},
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => {
|
||||
if p.at(kind.list_terminator()) {
|
||||
if p.at_ts(kind.list_terminators()) {
|
||||
Some(ListTerminatorKind::Regular)
|
||||
} else {
|
||||
// test_err unterminated_fstring_newline_recovery
|
||||
@@ -1176,23 +1177,13 @@ impl RecoveryContextKind {
|
||||
) || p.at_name_or_soft_keyword()
|
||||
}
|
||||
RecoveryContextKind::WithItems(_) => p.at_expr(),
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
RecoveryContextKind::InterpolatedStringElements(_) => matches!(
|
||||
p.current_token_kind(),
|
||||
// Literal element
|
||||
TokenKind::FStringMiddle | TokenKind::TStringMiddle
|
||||
// Expression element
|
||||
| TokenKind::Lbrace
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1281,8 +1272,8 @@ impl RecoveryContextKind {
|
||||
),
|
||||
},
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => match kind {
|
||||
InterpolatedStringElementsKind::Regular(string_kind) => ParseErrorType::OtherError(
|
||||
format!("Expected an element of or the end of the {string_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::FormatSpec => ParseErrorType::OtherError(
|
||||
"Expected an f-string or t-string element or a '}'".to_string(),
|
||||
@@ -1325,9 +1316,8 @@ bitflags! {
|
||||
const WITH_ITEMS_PARENTHESIZED = 1 << 25;
|
||||
const WITH_ITEMS_PARENTHESIZED_EXPRESSION = 1 << 26;
|
||||
const WITH_ITEMS_UNPARENTHESIZED = 1 << 28;
|
||||
const F_STRING_ELEMENTS = 1 << 29;
|
||||
const T_STRING_ELEMENTS = 1 << 30;
|
||||
const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 31;
|
||||
const FT_STRING_ELEMENTS = 1 << 29;
|
||||
const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1381,13 +1371,7 @@ impl RecoveryContext {
|
||||
WithItemKind::Unparenthesized => RecoveryContext::WITH_ITEMS_UNPARENTHESIZED,
|
||||
},
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => match kind {
|
||||
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::FString) => {
|
||||
RecoveryContext::F_STRING_ELEMENTS
|
||||
}
|
||||
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString) => {
|
||||
RecoveryContext::T_STRING_ELEMENTS
|
||||
}
|
||||
|
||||
InterpolatedStringElementsKind::Regular => RecoveryContext::FT_STRING_ELEMENTS,
|
||||
InterpolatedStringElementsKind::FormatSpec => {
|
||||
RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC
|
||||
}
|
||||
@@ -1458,11 +1442,8 @@ impl RecoveryContext {
|
||||
RecoveryContext::WITH_ITEMS_UNPARENTHESIZED => {
|
||||
RecoveryContextKind::WithItems(WithItemKind::Unparenthesized)
|
||||
}
|
||||
RecoveryContext::F_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
|
||||
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::FString),
|
||||
),
|
||||
RecoveryContext::T_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
|
||||
InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString),
|
||||
RecoveryContext::FT_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
|
||||
InterpolatedStringElementsKind::Regular,
|
||||
),
|
||||
RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC => {
|
||||
RecoveryContextKind::InterpolatedStringElements(
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/src/parser/tests.rs
|
||||
expression: error
|
||||
---
|
||||
ParseError {
|
||||
error: Lexical(
|
||||
LineContinuationError,
|
||||
),
|
||||
location: 3..4,
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/src/parser/tests.rs
|
||||
expression: error
|
||||
---
|
||||
ParseError {
|
||||
error: Lexical(
|
||||
TStringError(
|
||||
SingleRbrace,
|
||||
),
|
||||
),
|
||||
location: 8..9,
|
||||
}
|
||||
@@ -134,26 +134,3 @@ 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, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum InterpolatedStringKind {
|
||||
FString,
|
||||
TString,
|
||||
|
||||
@@ -124,5 +124,5 @@ Module(
|
||||
|
||||
|
|
||||
1 | f"{lambda x: x}"
|
||||
| ^ Syntax Error: Expected an element of or the end of the f-string
|
||||
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string
|
||||
|
|
||||
|
||||
@@ -221,7 +221,7 @@ Module(
|
||||
2 | 'hello'
|
||||
3 | f'world {x}
|
||||
4 | )
|
||||
| ^ Syntax Error: Expected an element of or the end of the f-string
|
||||
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-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 element of or the end of the t-string
|
||||
| ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string
|
||||
|
|
||||
|
||||
@@ -163,7 +163,7 @@ fn stem(path: &str) -> &str {
|
||||
}
|
||||
|
||||
/// Infer the [`Visibility`] of a module from its path.
|
||||
pub(crate) fn module_visibility(module: Module) -> Visibility {
|
||||
pub(crate) fn module_visibility(module: &Module) -> Visibility {
|
||||
match &module.source {
|
||||
ModuleSource::Path(path) => {
|
||||
if path.iter().any(|m| is_private_module(m)) {
|
||||
|
||||
@@ -223,7 +223,7 @@ impl<'a> Definitions<'a> {
|
||||
// visibility.
|
||||
let visibility = {
|
||||
match &definition {
|
||||
Definition::Module(module) => module_visibility(*module),
|
||||
Definition::Module(module) => module_visibility(module),
|
||||
Definition::Member(member) => match member.kind {
|
||||
MemberKind::Class(class) => {
|
||||
let parent = &definitions[member.parent];
|
||||
|
||||
@@ -238,7 +238,7 @@ fn to_lsp_diagnostic(
|
||||
let name = diagnostic.name();
|
||||
let body = diagnostic.body().to_string();
|
||||
let fix = diagnostic.fix();
|
||||
let suggestion = diagnostic.first_help_text();
|
||||
let suggestion = diagnostic.suggestion();
|
||||
let code = diagnostic.secondary_code();
|
||||
|
||||
let fix = fix.and_then(|fix| fix.applies(Applicability::Unsafe).then_some(fix));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -234,7 +234,7 @@ impl Workspace {
|
||||
start_location: source_code.line_column(msg.expect_range().start()).into(),
|
||||
end_location: source_code.line_column(msg.expect_range().end()).into(),
|
||||
fix: msg.fix().map(|fix| ExpandedFix {
|
||||
message: msg.first_help_text().map(ToString::to_string),
|
||||
message: msg.suggestion().map(ToString::to_string),
|
||||
edits: fix
|
||||
.edits()
|
||||
.iter()
|
||||
|
||||
8
crates/ty/docs/rules.md
generated
8
crates/ty/docs/rules.md
generated
@@ -16,7 +16,7 @@ Checks for byte-strings in type annotation positions.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Static analysis tools like ty can't analyze type annotations that use byte-string notation.
|
||||
Static analysis tools like ty can't analyse type annotations that use byte-string notation.
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -257,7 +257,7 @@ Checks for f-strings in type annotation positions.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Static analysis tools like ty can't analyze type annotations that use f-string notation.
|
||||
Static analysis tools like ty can't analyse type annotations that use f-string notation.
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -286,7 +286,7 @@ Checks for implicit concatenated strings in type annotation positions.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Static analysis tools like ty can't analyze type annotations that use implicit concatenated strings.
|
||||
Static analysis tools like ty can't analyse type annotations that use implicit concatenated strings.
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -1276,7 +1276,7 @@ Checks for raw-strings in type annotation positions.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Static analysis tools like ty can't analyze type annotations that use raw-string notation.
|
||||
Static analysis tools like ty can't analyse type annotations that use raw-string notation.
|
||||
|
||||
**Examples**
|
||||
|
||||
|
||||
@@ -156,12 +156,7 @@ 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);
|
||||
|
||||
@@ -676,7 +676,7 @@ fn invalid_include_pattern_concise_output() -> anyhow::Result<()> {
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: ty.toml:4:5: error[invalid-glob] Invalid include pattern: Too many stars at position 5
|
||||
Cause: error[invalid-glob] ty.toml:4:5: Invalid include pattern: Too many stars at position 5
|
||||
");
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -592,8 +592,8 @@ fn concise_diagnostics() -> anyhow::Result<()> {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
test.py:2:7: warning[unresolved-reference] Name `x` used when not defined
|
||||
test.py:3:7: error[non-subscriptable] Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
warning[unresolved-reference] test.py:2:7: Name `x` used when not defined
|
||||
error[non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
@@ -627,7 +627,7 @@ fn concise_revealed_type() -> anyhow::Result<()> {
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
test.py:5:13: info[revealed-type] Revealed type: `Literal["hello"]`
|
||||
info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]`
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -230,21 +230,6 @@ impl TestCase {
|
||||
fn system_file(&self, path: impl AsRef<SystemPath>) -> Result<File, FileError> {
|
||||
system_path_to_file(self.db(), path.as_ref())
|
||||
}
|
||||
|
||||
fn module<'c>(&'c self, name: &str) -> Module<'c> {
|
||||
resolve_module(self.db(), &ModuleName::new(name).unwrap()).expect("module to be present")
|
||||
}
|
||||
|
||||
fn sorted_submodule_names(&self, parent_module_name: &str) -> Vec<String> {
|
||||
let mut names = self
|
||||
.module(parent_module_name)
|
||||
.all_submodules(self.db())
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
}
|
||||
|
||||
trait MatchEvent {
|
||||
@@ -1413,7 +1398,7 @@ mod unix {
|
||||
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_project = case.project_path("bar/baz.py");
|
||||
let baz_file = baz.file(case.db()).unwrap();
|
||||
let baz_file = baz.file().unwrap();
|
||||
|
||||
assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
|
||||
assert_eq!(
|
||||
@@ -1488,7 +1473,7 @@ mod unix {
|
||||
|
||||
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_file = baz.file(case.db()).unwrap();
|
||||
let baz_file = baz.file().unwrap();
|
||||
let bar_baz = case.project_path("bar/baz.py");
|
||||
|
||||
let patched_bar_baz = case.project_path("patched/bar/baz.py");
|
||||
@@ -1609,10 +1594,7 @@ mod unix {
|
||||
"def baz(): ..."
|
||||
);
|
||||
assert_eq!(
|
||||
baz.file(case.db())
|
||||
.unwrap()
|
||||
.path(case.db())
|
||||
.as_system_path(),
|
||||
baz.file().unwrap().path(case.db()).as_system_path(),
|
||||
Some(&*baz_original)
|
||||
);
|
||||
|
||||
@@ -1909,9 +1891,19 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
|
||||
#[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!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -1920,7 +1912,7 @@ fn submodule_cache_invalidation_created() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
@@ -1940,9 +1932,19 @@ fn submodule_cache_invalidation_deleted() -> anyhow::Result<()> {
|
||||
("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!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
@@ -1954,7 +1956,7 @@ fn submodule_cache_invalidation_deleted() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -1966,9 +1968,19 @@ fn submodule_cache_invalidation_deleted() -> anyhow::Result<()> {
|
||||
#[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!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -1981,7 +1993,7 @@ fn submodule_cache_invalidation_created_then_deleted() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -1994,9 +2006,19 @@ fn submodule_cache_invalidation_created_then_deleted() -> anyhow::Result<()> {
|
||||
#[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!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -2007,7 +2029,7 @@ fn submodule_cache_invalidation_after_pyproject_created() -> anyhow::Result<()>
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
|
||||
@@ -19,15 +19,15 @@ ruff_python_trivia = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ty_python_semantic = { workspace = true }
|
||||
ty_project = { workspace = true, features = ["testing"] }
|
||||
|
||||
itertools = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ty_vendored = { workspace = true }
|
||||
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
|
||||
|
||||
@@ -1618,8 +1618,6 @@ 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]
|
||||
|
||||
117
crates/ty_ide/src/db.rs
Normal file
117
crates/ty_ide/src/db.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use ty_python_semantic::Db as SemanticDb;
|
||||
|
||||
#[salsa::db]
|
||||
pub trait Db: SemanticDb {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::Db;
|
||||
use ruff_db::Db as SourceDb;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use ty_python_semantic::{Db as SemanticDb, Program, default_lint_registry};
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Events,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let events = Events::default();
|
||||
Self {
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: ty_vendored::file_system().clone(),
|
||||
events,
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for TestDb {
|
||||
fn should_check_file(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
default_lint_registry()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for TestDb {}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
@@ -52,7 +52,9 @@ pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode
|
||||
if visitor.ancestors.is_empty() {
|
||||
visitor.ancestors.push(root);
|
||||
}
|
||||
CoveringNode::from_ancestors(visitor.ancestors)
|
||||
CoveringNode {
|
||||
nodes: visitor.ancestors,
|
||||
}
|
||||
}
|
||||
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
@@ -65,12 +67,6 @@ pub(crate) struct CoveringNode<'a> {
|
||||
}
|
||||
|
||||
impl<'a> CoveringNode<'a> {
|
||||
/// Creates a new `CoveringNode` from a list of ancestor nodes.
|
||||
/// The ancestors should be ordered from root to the covering node.
|
||||
pub(crate) fn from_ancestors(ancestors: Vec<AnyNodeRef<'a>>) -> Self {
|
||||
Self { nodes: ancestors }
|
||||
}
|
||||
|
||||
/// Returns the covering node found.
|
||||
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
|
||||
*self
|
||||
@@ -116,12 +112,6 @@ 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.
|
||||
///
|
||||
|
||||
@@ -2,62 +2,30 @@ pub use crate::goto_declaration::goto_declaration;
|
||||
pub use crate::goto_definition::goto_definition;
|
||||
pub use crate::goto_type_definition::goto_type_definition;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::find_node::covering_node;
|
||||
use crate::stub_mapping::StubMapper;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
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::types::definitions_for_keyword_argument;
|
||||
use ty_python_semantic::{
|
||||
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
|
||||
};
|
||||
use ty_python_semantic::{HasType, SemanticModel, definitions_for_name};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, 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),
|
||||
|
||||
/// Multi-part module names
|
||||
/// Handles both `import foo.bar` and `from foo.bar import baz` cases
|
||||
/// Go to on the module name of an import from
|
||||
/// ```py
|
||||
/// import foo.bar
|
||||
/// ^^^
|
||||
/// from foo.bar import baz
|
||||
/// ^^^
|
||||
/// from foo import bar
|
||||
/// ^^^
|
||||
/// ```
|
||||
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,
|
||||
},
|
||||
ImportedModule(&'a ast::StmtImportFrom),
|
||||
|
||||
/// Go to on the exception handler variable
|
||||
/// ```py
|
||||
@@ -72,10 +40,7 @@ pub(crate) enum GotoTarget<'a> {
|
||||
/// test(a = 1)
|
||||
/// ^
|
||||
/// ```
|
||||
KeywordArgument {
|
||||
keyword: &'a ast::Keyword,
|
||||
call_expression: &'a ast::ExprCall,
|
||||
},
|
||||
KeywordArgument(&'a ast::Keyword),
|
||||
|
||||
/// Go to on the rest parameter of a pattern match
|
||||
///
|
||||
@@ -144,22 +109,25 @@ 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::ImportSymbolAlias { alias, .. } => alias.inferred_type(model),
|
||||
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
|
||||
GotoTarget::Alias(alias) => alias.inferred_type(model),
|
||||
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
|
||||
GotoTarget::KeywordArgument { keyword, .. } => keyword.value.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)
|
||||
}
|
||||
// TODO: Support identifier targets
|
||||
GotoTarget::PatternMatchRest(_)
|
||||
| GotoTarget::PatternKeywordArgument(_)
|
||||
| GotoTarget::PatternMatchStarName(_)
|
||||
| GotoTarget::PatternMatchAsName(_)
|
||||
| GotoTarget::ImportModuleComponent { .. }
|
||||
| GotoTarget::ImportedModule(_)
|
||||
| GotoTarget::TypeParamTypeVarName(_)
|
||||
| GotoTarget::TypeParamParamSpecName(_)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(_)
|
||||
@@ -174,7 +142,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>,
|
||||
@@ -225,317 +193,38 @@ impl GotoTarget<'_> {
|
||||
}))
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
GotoTarget::KeywordArgument(keyword) => {
|
||||
// Find the call expression that contains this keyword
|
||||
let module = parsed_module(db, file).load(db);
|
||||
|
||||
// For exception variables, they are their own definitions (like parameters)
|
||||
GotoTarget::ExceptVariable(except_handler) => {
|
||||
if let Some(name) = &except_handler.name {
|
||||
let range = name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget::new(
|
||||
file, range,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// For pattern match rest variables, they are their own definitions
|
||||
GotoTarget::PatternMatchRest(pattern_mapping) => {
|
||||
if let Some(rest_name) = &pattern_mapping.rest {
|
||||
let range = rest_name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget::new(
|
||||
file, range,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// For pattern match as names, they are their own definitions
|
||||
GotoTarget::PatternMatchAsName(pattern_as) => {
|
||||
if let Some(name) = &pattern_as.name {
|
||||
let range = name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget::new(
|
||||
file, range,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
// Use the keyword's range to find the containing call expression
|
||||
let covering_node = covering_node(module.syntax().into(), keyword.range())
|
||||
.find_first(|node| matches!(node, AnyNodeRef::ExprCall(_)))
|
||||
.ok()?;
|
||||
|
||||
if let AnyNodeRef::ExprCall(call_expr) = covering_node.node() {
|
||||
let definitions =
|
||||
definitions_for_keyword_argument(db, file, keyword, call_expr);
|
||||
return definitions_to_navigation_targets(db, stub_mapper, definitions);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the text representation of this goto target.
|
||||
/// Returns `None` if no meaningful string representation can be provided.
|
||||
/// This is used by the "references" feature, which looks for references
|
||||
/// to this goto target.
|
||||
pub(crate) fn to_string(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => match expression {
|
||||
ast::ExprRef::Name(name) => Some(Cow::Borrowed(name.id.as_str())),
|
||||
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
|
||||
_ => None,
|
||||
},
|
||||
GotoTarget::FunctionDef(function) => Some(Cow::Borrowed(function.name.as_str())),
|
||||
GotoTarget::ClassDef(class) => Some(Cow::Borrowed(class.name.as_str())),
|
||||
GotoTarget::Parameter(parameter) => Some(Cow::Borrowed(parameter.name.as_str())),
|
||||
GotoTarget::ImportSymbolAlias { alias, .. } => {
|
||||
if let Some(asname) = &alias.asname {
|
||||
Some(Cow::Borrowed(asname.as_str()))
|
||||
} else {
|
||||
Some(Cow::Borrowed(alias.name.as_str()))
|
||||
}
|
||||
}
|
||||
GotoTarget::ImportModuleComponent {
|
||||
module_name,
|
||||
component_index,
|
||||
..
|
||||
} => {
|
||||
let components: Vec<&str> = module_name.split('.').collect();
|
||||
if let Some(component) = components.get(*component_index) {
|
||||
Some(Cow::Borrowed(*component))
|
||||
} else {
|
||||
Some(Cow::Borrowed(module_name))
|
||||
}
|
||||
}
|
||||
GotoTarget::ImportModuleAlias { alias } => {
|
||||
if let Some(asname) = &alias.asname {
|
||||
Some(Cow::Borrowed(asname.as_str()))
|
||||
} else {
|
||||
Some(Cow::Borrowed(alias.name.as_str()))
|
||||
}
|
||||
}
|
||||
GotoTarget::ExceptVariable(except) => {
|
||||
Some(Cow::Borrowed(except.name.as_ref()?.as_str()))
|
||||
}
|
||||
GotoTarget::KeywordArgument { keyword, .. } => {
|
||||
Some(Cow::Borrowed(keyword.arg.as_ref()?.as_str()))
|
||||
}
|
||||
GotoTarget::PatternMatchRest(rest) => Some(Cow::Borrowed(rest.rest.as_ref()?.as_str())),
|
||||
GotoTarget::PatternKeywordArgument(keyword) => {
|
||||
Some(Cow::Borrowed(keyword.attr.as_str()))
|
||||
}
|
||||
GotoTarget::PatternMatchStarName(star) => {
|
||||
Some(Cow::Borrowed(star.name.as_ref()?.as_str()))
|
||||
}
|
||||
GotoTarget::PatternMatchAsName(as_name) => {
|
||||
Some(Cow::Borrowed(as_name.name.as_ref()?.as_str()))
|
||||
}
|
||||
GotoTarget::TypeParamTypeVarName(type_var) => {
|
||||
Some(Cow::Borrowed(type_var.name.as_str()))
|
||||
}
|
||||
GotoTarget::TypeParamParamSpecName(spec) => Some(Cow::Borrowed(spec.name.as_str())),
|
||||
GotoTarget::TypeParamTypeVarTupleName(tuple) => {
|
||||
Some(Cow::Borrowed(tuple.name.as_str()))
|
||||
}
|
||||
GotoTarget::NonLocal { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())),
|
||||
GotoTarget::Globals { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `GotoTarget` from a `CoveringNode` and an offset within the node
|
||||
pub(crate) fn from_covering_node<'a>(
|
||||
covering_node: &crate::find_node::CoveringNode<'a>,
|
||||
offset: TextSize,
|
||||
) -> Option<GotoTarget<'a>> {
|
||||
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
|
||||
|
||||
match covering_node.node() {
|
||||
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
|
||||
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)) => {
|
||||
// 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)) => {
|
||||
// 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))
|
||||
}
|
||||
Some(AnyNodeRef::PatternKeyword(keyword)) => {
|
||||
Some(GotoTarget::PatternKeywordArgument(keyword))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchStar(star)) => {
|
||||
Some(GotoTarget::PatternMatchStarName(star))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
|
||||
Some(GotoTarget::PatternMatchAsName(as_pattern))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVar(var)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarName(var))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
|
||||
Some(GotoTarget::TypeParamParamSpecName(bound))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||
}
|
||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
||||
Some(GotoTarget::Expression(attribute.into()))
|
||||
}
|
||||
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||
None => None,
|
||||
Some(parent) => {
|
||||
tracing::debug!(
|
||||
"Missing `GoToTarget` for identifier with parent {:?}",
|
||||
parent.kind()
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for GotoTarget<'_> {
|
||||
@@ -545,13 +234,10 @@ impl Ranged for GotoTarget<'_> {
|
||||
GotoTarget::FunctionDef(function) => function.name.range,
|
||||
GotoTarget::ClassDef(class) => class.name.range,
|
||||
GotoTarget::Parameter(parameter) => parameter.name.range,
|
||||
GotoTarget::ImportSymbolAlias { range, .. } => *range,
|
||||
GotoTarget::ImportModuleComponent {
|
||||
component_range, ..
|
||||
} => *component_range,
|
||||
GotoTarget::ImportModuleAlias { alias } => alias.asname.as_ref().unwrap().range,
|
||||
GotoTarget::Alias(alias) => alias.name.range,
|
||||
GotoTarget::ImportedModule(module) => module.module.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,
|
||||
@@ -590,7 +276,11 @@ fn convert_resolved_definitions_to_targets(
|
||||
}
|
||||
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
|
||||
// For file ranges, navigate to the specific range within the file
|
||||
crate::NavigationTarget::new(file_range.file(), file_range.range())
|
||||
crate::NavigationTarget {
|
||||
file: file_range.file(),
|
||||
focus_range: file_range.range(),
|
||||
full_range: file_range.range(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -633,57 +323,53 @@ pub(crate) fn find_goto_target(
|
||||
.find_first(|node| node.is_identifier() || node.is_expression())
|
||||
.ok()?;
|
||||
|
||||
GotoTarget::from_covering_node(&covering_node, offset)
|
||||
}
|
||||
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
|
||||
|
||||
/// 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(db) {
|
||||
return Some(crate::NavigationTargets::single(
|
||||
crate::NavigationTarget::new(module_file, TextRange::default()),
|
||||
));
|
||||
match covering_node.node() {
|
||||
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
|
||||
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::ExceptHandlerExceptHandler(handler)) => {
|
||||
Some(GotoTarget::ExceptVariable(handler))
|
||||
}
|
||||
}
|
||||
Some(AnyNodeRef::Keyword(keyword)) => Some(GotoTarget::KeywordArgument(keyword)),
|
||||
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
|
||||
Some(GotoTarget::PatternMatchRest(mapping))
|
||||
}
|
||||
Some(AnyNodeRef::PatternKeyword(keyword)) => {
|
||||
Some(GotoTarget::PatternKeywordArgument(keyword))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchStar(star)) => {
|
||||
Some(GotoTarget::PatternMatchStarName(star))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
|
||||
Some(GotoTarget::PatternMatchAsName(as_pattern))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVar(var)) => Some(GotoTarget::TypeParamTypeVarName(var)),
|
||||
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
|
||||
Some(GotoTarget::TypeParamParamSpecName(bound))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||
}
|
||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
||||
Some(GotoTarget::Expression(attribute.into()))
|
||||
}
|
||||
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||
None => None,
|
||||
Some(parent) => {
|
||||
tracing::debug!(
|
||||
"Missing `GoToTarget` for identifier with parent {:?}",
|
||||
parent.kind()
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -611,229 +610,6 @@ def another_helper():
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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(
|
||||
@@ -1350,7 +1126,7 @@ class MyClass:
|
||||
|
||||
impl IntoDiagnostic for GotoDeclarationDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
|
||||
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
@@ -29,569 +29,3 @@ pub fn goto_definition(
|
||||
value: definition_targets,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::tests::{CursorTest, IntoDiagnostic};
|
||||
use crate::{NavigationTarget, goto_definition};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// goto-definition on a module should go to the .py not the .pyi
|
||||
///
|
||||
/// TODO: this currently doesn't work right! This is especially surprising
|
||||
/// because [`goto_definition_stub_map_module_ref`] works fine.
|
||||
#[test]
|
||||
fn goto_definition_stub_map_module_import() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymo<CURSOR>dule import my_function
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.pyi:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | def my_function(): ...
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:6
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
| ^^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a module ref should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_module_ref() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
import mymodule
|
||||
x = mymo<CURSOR>dule
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | def my_function():
|
||||
3 | return "hello"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | import mymodule
|
||||
3 | x = mymodule
|
||||
| ^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a function call should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_function() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import my_function
|
||||
print(my_func<CURSOR>tion())
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
|
||||
def other_function():
|
||||
return "other"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_function(): ...
|
||||
|
||||
def other_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:5
|
||||
|
|
||||
2 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
3 | return "hello"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
3 | print(my_function())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a function that's redefined many times in the impl .py
|
||||
///
|
||||
/// Currently this yields all instances. There's an argument for only yielding
|
||||
/// the final one since that's the one "exported" but, this is consistent for
|
||||
/// how we do file-local goto-definition.
|
||||
#[test]
|
||||
fn goto_definition_stub_map_function_redefine() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import my_function
|
||||
print(my_func<CURSOR>tion())
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
|
||||
def my_function():
|
||||
return "hello again"
|
||||
|
||||
def my_function():
|
||||
return "we can't keep doing this"
|
||||
|
||||
def other_function():
|
||||
return "other"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_function(): ...
|
||||
|
||||
def other_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:5
|
||||
|
|
||||
2 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
3 | return "hello"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
3 | print(my_function())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:5:5
|
||||
|
|
||||
3 | return "hello"
|
||||
4 |
|
||||
5 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
6 | return "hello again"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
3 | print(my_function())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:8:5
|
||||
|
|
||||
6 | return "hello again"
|
||||
7 |
|
||||
8 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
9 | return "we can't keep doing this"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
3 | print(my_function())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a class ref go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_ref() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyClass
|
||||
x = MyC<CURSOR>lass
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val: bool): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^^^
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
3 | x = MyClass
|
||||
| ^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a class init should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_init() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyClass
|
||||
x = MyCl<CURSOR>ass(0)
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val: bool): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^^^
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
3 | x = MyClass(0)
|
||||
| ^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a class method should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_method() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyClass
|
||||
x = MyClass(0)
|
||||
x.act<CURSOR>ion()
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
def action(self):
|
||||
print(self.val)
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val: bool): ...
|
||||
def action(self): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:5:9
|
||||
|
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
5 | def action(self):
|
||||
| ^^^^^^
|
||||
6 | print(self.val)
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:1
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
3 | x = MyClass(0)
|
||||
4 | x.action()
|
||||
| ^^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a class function should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_function() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyClass
|
||||
x = MyClass.act<CURSOR>ion()
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
def action():
|
||||
print("hi!")
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val: bool): ...
|
||||
def action(): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:5:9
|
||||
|
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
5 | def action():
|
||||
| ^^^^^^
|
||||
6 | print("hi!")
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
3 | x = MyClass.action()
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a class import should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_import() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyC<CURSOR>lass
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass: ...
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass: ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:7
|
||||
|
|
||||
2 | class MyClass: ...
|
||||
| ^^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:22
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
| ^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_definition(&self) -> String {
|
||||
let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset)
|
||||
else {
|
||||
return "No goto target found".to_string();
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return "No definitions found".to_string();
|
||||
}
|
||||
|
||||
let source = targets.range;
|
||||
self.render_diagnostics(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| GotoDefinitionDiagnostic::new(source, &target)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoDefinitionDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
}
|
||||
|
||||
impl GotoDefinitionDiagnostic {
|
||||
fn new(source: FileRange, target: &NavigationTarget) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target: FileRange::new(target.file(), target.focus_range()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for GotoDefinitionDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
let mut main = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("goto-definition")),
|
||||
Severity::Info,
|
||||
"Definition".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.target.file()).with_range(self.target.range()),
|
||||
));
|
||||
main.sub(source);
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -199,14 +198,14 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
@@ -228,14 +227,14 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:22
|
||||
@@ -344,14 +343,14 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
@@ -379,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:337:7
|
||||
--> stdlib/builtins.pyi:338:7
|
||||
|
|
||||
335 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
336 |
|
||||
337 | class int:
|
||||
336 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
337 |
|
||||
338 | class int:
|
||||
| ^^^
|
||||
338 | """int([x]) -> integer
|
||||
339 | int(x, base=10) -> integer
|
||||
339 | """int([x]) -> integer
|
||||
340 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
@@ -413,14 +412,14 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:2888:7
|
||||
--> stdlib/builtins.pyi:2892:7
|
||||
|
|
||||
2886 | """See PEP 585"""
|
||||
2887 |
|
||||
2888 | class dict(MutableMapping[_KT, _VT]):
|
||||
2890 | """See PEP 585"""
|
||||
2891 |
|
||||
2892 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
2889 | """dict() -> new empty dictionary
|
||||
2890 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
2893 | """dict() -> new empty dictionary
|
||||
2894 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:5
|
||||
@@ -444,14 +443,14 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
@@ -537,14 +536,14 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:27
|
||||
@@ -568,13 +567,13 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/types.pyi:921:11
|
||||
--> stdlib/types.pyi:922:11
|
||||
|
|
||||
919 | if sys.version_info >= (3, 10):
|
||||
920 | @final
|
||||
921 | class NoneType:
|
||||
920 | if sys.version_info >= (3, 10):
|
||||
921 | @final
|
||||
922 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
922 | """The type of the None singleton."""
|
||||
923 | """The type of the None singleton."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
@@ -585,14 +584,14 @@ f(**kwargs<CURSOR>)
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:890:7
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
889 |
|
||||
890 | class str(Sequence[str]):
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
891 | """str(object='') -> str
|
||||
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
@@ -641,7 +640,7 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
|
||||
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
@@ -156,8 +156,9 @@ mod tests {
|
||||
};
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPathBuf};
|
||||
use ty_project::ProjectMetadata;
|
||||
use ty_python_semantic::{
|
||||
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
|
||||
};
|
||||
@@ -166,10 +167,7 @@ mod tests {
|
||||
const START: &str = "<START>";
|
||||
const END: &str = "<END>";
|
||||
|
||||
let mut db = ty_project::TestDb::new(ProjectMetadata::new(
|
||||
"test".into(),
|
||||
SystemPathBuf::from("/"),
|
||||
));
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let start = source.find(START);
|
||||
let end = source
|
||||
@@ -207,7 +205,7 @@ mod tests {
|
||||
}
|
||||
|
||||
pub(super) struct InlayHintTest {
|
||||
pub(super) db: ty_project::TestDb,
|
||||
pub(super) db: TestDb,
|
||||
pub(super) file: File,
|
||||
pub(super) range: TextRange,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod completion;
|
||||
mod db;
|
||||
mod docstring;
|
||||
mod find_node;
|
||||
mod goto;
|
||||
@@ -8,18 +9,17 @@ mod goto_type_definition;
|
||||
mod hover;
|
||||
mod inlay_hints;
|
||||
mod markup;
|
||||
mod references;
|
||||
mod semantic_tokens;
|
||||
mod signature_help;
|
||||
mod stub_mapping;
|
||||
|
||||
pub use completion::completion;
|
||||
pub use db::Db;
|
||||
pub use docstring::get_parameter_documentation;
|
||||
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
|
||||
pub use hover::hover;
|
||||
pub use inlay_hints::inlay_hints;
|
||||
pub use markup::MarkupKind;
|
||||
pub use references::references;
|
||||
pub use semantic_tokens::{
|
||||
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,
|
||||
};
|
||||
@@ -29,7 +29,6 @@ use ruff_db::files::{File, FileRange};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use ty_project::Db;
|
||||
use ty_python_semantic::types::{Type, TypeDefinition};
|
||||
|
||||
/// Information associated with a text range.
|
||||
@@ -88,15 +87,6 @@ pub struct NavigationTarget {
|
||||
}
|
||||
|
||||
impl NavigationTarget {
|
||||
/// Creates a new `NavigationTarget` where the focus and full range are identical.
|
||||
pub fn new(file: File, range: TextRange) -> Self {
|
||||
Self {
|
||||
file,
|
||||
focus_range: range,
|
||||
full_range: range,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
@@ -221,13 +211,13 @@ impl HasNavigationTargets for TypeDefinition<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use ruff_db::Db;
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
|
||||
use ruff_db::files::{File, system_path_to_file};
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_text_size::TextSize;
|
||||
use ty_project::ProjectMetadata;
|
||||
use ty_python_semantic::{
|
||||
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
|
||||
};
|
||||
@@ -241,7 +231,7 @@ mod tests {
|
||||
}
|
||||
|
||||
pub(super) struct CursorTest {
|
||||
pub(super) db: ty_project::TestDb,
|
||||
pub(super) db: TestDb,
|
||||
pub(super) cursor: Cursor,
|
||||
_insta_settings_guard: SettingsBindDropGuard,
|
||||
}
|
||||
@@ -296,13 +286,8 @@ mod tests {
|
||||
|
||||
impl CursorTestBuilder {
|
||||
pub(super) fn build(&self) -> CursorTest {
|
||||
let mut db = ty_project::TestDb::new(ProjectMetadata::new(
|
||||
"test".into(),
|
||||
SystemPathBuf::from("/"),
|
||||
));
|
||||
|
||||
let mut db = TestDb::new();
|
||||
let mut cursor: Option<Cursor> = None;
|
||||
|
||||
for &Source {
|
||||
ref path,
|
||||
ref contents,
|
||||
@@ -311,19 +296,19 @@ mod tests {
|
||||
{
|
||||
db.write_file(path, contents)
|
||||
.expect("write to memory file system to be successful");
|
||||
let Some(offset) = cursor_offset else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let file = system_path_to_file(&db, path).expect("newly written file to existing");
|
||||
|
||||
if let Some(offset) = cursor_offset {
|
||||
// This assert should generally never trip, since
|
||||
// we have an assert on `CursorTestBuilder::source`
|
||||
// to ensure we never have more than one marker.
|
||||
assert!(
|
||||
cursor.is_none(),
|
||||
"found more than one source that contains `<CURSOR>`"
|
||||
);
|
||||
cursor = Some(Cursor { file, offset });
|
||||
}
|
||||
// This assert should generally never trip, since
|
||||
// we have an assert on `CursorTestBuilder::source`
|
||||
// to ensure we never have more than one marker.
|
||||
assert!(
|
||||
cursor.is_none(),
|
||||
"found more than one source that contains `<CURSOR>`"
|
||||
);
|
||||
cursor = Some(Cursor { file, offset });
|
||||
}
|
||||
|
||||
let search_paths = SearchPathSettings::new(vec![SystemPathBuf::from("/")])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -664,26 +664,6 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Stmt::Nonlocal(nonlocal_stmt) => {
|
||||
// Handle nonlocal statements - classify identifiers as variables
|
||||
for identifier in &nonlocal_stmt.names {
|
||||
self.add_token(
|
||||
identifier.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::Stmt::Global(global_stmt) => {
|
||||
// Handle global statements - classify identifiers as variables
|
||||
for identifier in &global_stmt.names {
|
||||
self.add_token(
|
||||
identifier.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// For all other statement types, let the default visitor handle them
|
||||
walk_stmt(self, stmt);
|
||||
@@ -851,71 +831,6 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_except_handler(&mut self, except_handler: &ast::ExceptHandler) {
|
||||
match except_handler {
|
||||
ast::ExceptHandler::ExceptHandler(handler) => {
|
||||
// Visit the exception type expression if present
|
||||
if let Some(type_expr) = &handler.type_ {
|
||||
self.visit_expr(type_expr);
|
||||
}
|
||||
|
||||
// Handle the exception variable name (after "as")
|
||||
if let Some(name) = &handler.name {
|
||||
self.add_token(
|
||||
name.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
|
||||
// Visit the handler body
|
||||
self.visit_body(&handler.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &ast::Pattern) {
|
||||
match pattern {
|
||||
ast::Pattern::MatchAs(pattern_as) => {
|
||||
// Visit the nested pattern first to maintain source order
|
||||
if let Some(nested_pattern) = &pattern_as.pattern {
|
||||
self.visit_pattern(nested_pattern);
|
||||
}
|
||||
|
||||
// Now add the "as" variable name token
|
||||
if let Some(name) = &pattern_as.name {
|
||||
self.add_token(
|
||||
name.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::Pattern::MatchMapping(pattern_mapping) => {
|
||||
// Visit keys and patterns in source order by interleaving them
|
||||
for (key, nested_pattern) in
|
||||
pattern_mapping.keys.iter().zip(&pattern_mapping.patterns)
|
||||
{
|
||||
self.visit_expr(key);
|
||||
self.visit_pattern(nested_pattern);
|
||||
}
|
||||
|
||||
// Handle the rest parameter (after "**") - this comes last
|
||||
if let Some(rest_name) = &pattern_mapping.rest {
|
||||
self.add_token(
|
||||
rest_name.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// For all other pattern types, use the default walker
|
||||
ruff_python_ast::visitor::source_order::walk_pattern(self, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2027,200 +1942,4 @@ complex_fstring = f"User: {name.upper()}, Count: {len(data)}, Hex: {value:x}"<CU
|
||||
"x" @ 414..415: String
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonlocal_and_global_statements() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
x = "global_value"
|
||||
y = "another_global"
|
||||
|
||||
def outer():
|
||||
x = "outer_value"
|
||||
z = "outer_local"
|
||||
|
||||
def inner():
|
||||
nonlocal x, z # These should be variable tokens
|
||||
global y # This should be a variable token
|
||||
x = "modified"
|
||||
y = "modified_global"
|
||||
z = "modified_local"
|
||||
|
||||
def deeper():
|
||||
nonlocal x # Variable token
|
||||
global y, x # Both should be variable tokens
|
||||
return x + y
|
||||
|
||||
return deeper
|
||||
|
||||
return inner<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
|
||||
|
||||
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
|
||||
"x" @ 1..2: Variable
|
||||
"/"global_value/"" @ 5..19: String
|
||||
"y" @ 20..21: Variable
|
||||
"/"another_global/"" @ 24..40: String
|
||||
"outer" @ 46..51: Function [definition]
|
||||
"x" @ 59..60: Variable
|
||||
"/"outer_value/"" @ 63..76: String
|
||||
"z" @ 81..82: Variable
|
||||
"/"outer_local/"" @ 85..98: String
|
||||
"inner" @ 112..117: Function [definition]
|
||||
"x" @ 138..139: Variable
|
||||
"z" @ 141..142: Variable
|
||||
"y" @ 193..194: Variable
|
||||
"x" @ 243..244: Variable
|
||||
"/"modified/"" @ 247..257: String
|
||||
"y" @ 266..267: Variable
|
||||
"/"modified_global/"" @ 270..287: String
|
||||
"z" @ 296..297: Variable
|
||||
"/"modified_local/"" @ 300..316: String
|
||||
"deeper" @ 338..344: Function [definition]
|
||||
"x" @ 369..370: Variable
|
||||
"y" @ 410..411: Variable
|
||||
"x" @ 413..414: Variable
|
||||
"x" @ 469..470: Variable
|
||||
"y" @ 473..474: Variable
|
||||
"deeper" @ 499..505: Function
|
||||
"inner" @ 522..527: Function
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonlocal_global_edge_cases() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
# Single variable statements
|
||||
def test():
|
||||
global x
|
||||
nonlocal y
|
||||
|
||||
# Multiple variables in one statement
|
||||
global a, b, c
|
||||
nonlocal d, e, f
|
||||
|
||||
return x + y + a + b + c + d + e + f<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
|
||||
|
||||
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
|
||||
"test" @ 34..38: Function [definition]
|
||||
"x" @ 53..54: Variable
|
||||
"y" @ 68..69: Variable
|
||||
"a" @ 128..129: Variable
|
||||
"b" @ 131..132: Variable
|
||||
"c" @ 134..135: Variable
|
||||
"d" @ 149..150: Variable
|
||||
"e" @ 152..153: Variable
|
||||
"f" @ 155..156: Variable
|
||||
"x" @ 173..174: Variable
|
||||
"y" @ 177..178: Variable
|
||||
"a" @ 181..182: Variable
|
||||
"b" @ 185..186: Variable
|
||||
"c" @ 189..190: Variable
|
||||
"d" @ 193..194: Variable
|
||||
"e" @ 197..198: Variable
|
||||
"f" @ 201..202: Variable
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_matching() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def process_data(data):
|
||||
match data:
|
||||
case {"name": name, "age": age, **rest} as person:
|
||||
print(f"Person {name}, age {age}, extra: {rest}")
|
||||
return person
|
||||
case [first, *remaining] as sequence:
|
||||
print(f"First: {first}, remaining: {remaining}")
|
||||
return sequence
|
||||
case value as fallback:
|
||||
print(f"Fallback: {fallback}")
|
||||
return fallback<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
|
||||
|
||||
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
|
||||
"process_data" @ 5..17: Function [definition]
|
||||
"data" @ 18..22: Parameter
|
||||
"data" @ 35..39: Variable
|
||||
"/"name/"" @ 55..61: String
|
||||
"name" @ 63..67: Variable
|
||||
"/"age/"" @ 69..74: String
|
||||
"age" @ 76..79: Variable
|
||||
"rest" @ 83..87: Variable
|
||||
"person" @ 92..98: Variable
|
||||
"print" @ 112..117: Function
|
||||
"Person " @ 120..127: String
|
||||
"name" @ 128..132: Variable
|
||||
", age " @ 133..139: String
|
||||
"age" @ 140..143: Variable
|
||||
", extra: " @ 144..153: String
|
||||
"rest" @ 154..158: Variable
|
||||
"person" @ 181..187: Variable
|
||||
"first" @ 202..207: Variable
|
||||
"sequence" @ 224..232: Variable
|
||||
"print" @ 246..251: Function
|
||||
"First: " @ 254..261: String
|
||||
"first" @ 262..267: Variable
|
||||
", remaining: " @ 268..281: String
|
||||
"remaining" @ 282..291: Variable
|
||||
"sequence" @ 314..322: Variable
|
||||
"value" @ 336..341: Variable
|
||||
"fallback" @ 345..353: Variable
|
||||
"print" @ 367..372: Function
|
||||
"Fallback: " @ 375..385: String
|
||||
"fallback" @ 386..394: Variable
|
||||
"fallback" @ 417..425: Variable
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exception_handlers() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
try:
|
||||
x = 1 / 0
|
||||
except ValueError as ve:
|
||||
print(ve)
|
||||
except (TypeError, RuntimeError) as re:
|
||||
print(re)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
pass<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
|
||||
|
||||
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
|
||||
"x" @ 10..11: Variable
|
||||
"1" @ 14..15: Number
|
||||
"0" @ 18..19: Number
|
||||
"ValueError" @ 27..37: Class
|
||||
"ve" @ 41..43: Variable
|
||||
"print" @ 49..54: Function
|
||||
"ve" @ 55..57: Variable
|
||||
"TypeError" @ 67..76: Class
|
||||
"RuntimeError" @ 78..90: Class
|
||||
"re" @ 95..97: Variable
|
||||
"print" @ 103..108: Function
|
||||
"re" @ 109..111: Variable
|
||||
"Exception" @ 120..129: Class
|
||||
"e" @ 133..134: Variable
|
||||
"print" @ 140..145: Function
|
||||
"e" @ 146..147: Variable
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user