Compare commits
92 Commits
micha/fixp
...
cjm/generi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
381b7d0982 | ||
|
|
f68dbfdef1 | ||
|
|
41fa082414 | ||
|
|
c7b6108cb8 | ||
|
|
a97e72fb5e | ||
|
|
0d6fafd0f9 | ||
|
|
2eb2d5359b | ||
|
|
f549dfe39d | ||
|
|
6b64630635 | ||
|
|
d545b5bfd2 | ||
|
|
b2d9f59937 | ||
|
|
d7ef01401c | ||
|
|
c9031ce59f | ||
|
|
138ab91def | ||
|
|
550b8be552 | ||
|
|
bdccb37b4a | ||
|
|
3ccc0edfe4 | ||
|
|
6b3ff6f5b8 | ||
|
|
6f8f7506b4 | ||
|
|
797eb70904 | ||
|
|
be6ec613db | ||
|
|
fcd858e0c8 | ||
|
|
d944a1397e | ||
|
|
d3f3d92df3 | ||
|
|
38c00dfad5 | ||
|
|
d6280c5aea | ||
|
|
a34240a3f0 | ||
|
|
b86c7bbf7c | ||
|
|
d7c54ba8c4 | ||
|
|
c38d6e8045 | ||
|
|
2bfd7b1816 | ||
|
|
c1cfb43bf0 | ||
|
|
99555b775c | ||
|
|
b398b83631 | ||
|
|
bc7b30364d | ||
|
|
5792ed15da | ||
|
|
8845a13efb | ||
|
|
669855d2b5 | ||
|
|
ff7ebecf89 | ||
|
|
7e8ba2b68e | ||
|
|
0bb8cbdf07 | ||
|
|
2923c55698 | ||
|
|
316e406ca4 | ||
|
|
5ecd560c6f | ||
|
|
b765dc48e9 | ||
|
|
cd1d906ffa | ||
|
|
235b74a310 | ||
|
|
40fd52dde0 | ||
|
|
fd1eb3d801 | ||
|
|
882a1a702e | ||
|
|
b4a1ebdfe3 | ||
|
|
7a48477c67 | ||
|
|
346e82b572 | ||
|
|
5ea3a52c8a | ||
|
|
90272ad85a | ||
|
|
e9da1750a1 | ||
|
|
25e13debc0 | ||
|
|
249a852a6e | ||
|
|
861ef2504e | ||
|
|
b71ef8a26e | ||
|
|
50c780fc8b | ||
|
|
244ea27d5f | ||
|
|
2c4cbb6e29 | ||
|
|
d1bb10a66b | ||
|
|
2370297cde | ||
|
|
a137cb18d4 | ||
|
|
03a4d56624 | ||
|
|
642eac452d | ||
|
|
c1b875799b | ||
|
|
6cd8a49638 | ||
|
|
12ce445ff7 | ||
|
|
f46ed8d410 | ||
|
|
6c177e2bbe | ||
|
|
3d2485eb1b | ||
|
|
f78367979e | ||
|
|
b705664d49 | ||
|
|
f51f1f7153 | ||
|
|
9b694ada82 | ||
|
|
4d81a41107 | ||
|
|
981bd70d39 | ||
|
|
0763331f7f | ||
|
|
da8540862d | ||
|
|
6a5533c44c | ||
|
|
d608eae126 | ||
|
|
067a8ac574 | ||
|
|
5eb215e8e5 | ||
|
|
91aa853b9c | ||
|
|
57bf7dfbd9 | ||
|
|
67cd94ed64 | ||
|
|
aac862822f | ||
|
|
3755ac9fac | ||
|
|
4f890b2867 |
7
.github/mypy-primer-ty.toml
vendored
Normal file
7
.github/mypy-primer-ty.toml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
#:schema ../ty.schema.json
|
||||
# Configuration overrides for the mypy primer run
|
||||
|
||||
# Enable off-by-default rules.
|
||||
[rules]
|
||||
possibly-unresolved-reference = "warn"
|
||||
unused-ignore-comment = "warn"
|
||||
12
.github/workflows/build-docker.yml
vendored
12
.github/workflows/build-docker.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
${{ env.TAG_PATTERNS }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
20
.github/workflows/ci.yaml
vendored
20
.github/workflows/ci.yaml
vendored
@@ -239,11 +239,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -297,11 +297,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -407,11 +407,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -437,7 +437,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
|
||||
uses: cargo-bins/cargo-binstall@13f9d60d5358393bf14644dba56d9f123bc5d595 # v1.12.4
|
||||
with:
|
||||
tool: cargo-fuzz@0.11.2
|
||||
- name: "Install cargo-fuzz"
|
||||
@@ -692,7 +692,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
|
||||
- uses: cargo-bins/cargo-binstall@13f9d60d5358393bf14644dba56d9f123bc5d595 # v1.12.4
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -908,7 +908,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
|
||||
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
|
||||
4
.github/workflows/mypy_primer.yaml
vendored
4
.github/workflows/mypy_primer.yaml
vendored
@@ -50,6 +50,10 @@ jobs:
|
||||
run: |
|
||||
cd ruff
|
||||
|
||||
echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
|
||||
mkdir -p ~/.config/ty
|
||||
cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
|
||||
|
||||
PRIMER_SELECTOR="$(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt)"
|
||||
|
||||
echo "new commit"
|
||||
|
||||
@@ -5,6 +5,7 @@ exclude: |
|
||||
.github/workflows/release.yml|
|
||||
crates/ty_vendored/vendor/.*|
|
||||
crates/ty_project/resources/.*|
|
||||
crates/ty/docs/(configuration|rules|cli).md|
|
||||
crates/ruff_benchmark/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_linter/src/rules/.*/snapshots/.*|
|
||||
@@ -79,7 +80,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.8
|
||||
rev: v0.11.9
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -97,7 +98,7 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.6.0
|
||||
rev: v1.7.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## 0.11.9
|
||||
|
||||
### Preview features
|
||||
|
||||
- Default to latest supported Python version for version-related syntax errors ([#17529](https://github.com/astral-sh/ruff/pull/17529))
|
||||
- Implement deferred annotations for Python 3.14 ([#17658](https://github.com/astral-sh/ruff/pull/17658))
|
||||
- \[`airflow`\] Fix `SQLTableCheckOperator` typo (`AIR302`) ([#17946](https://github.com/astral-sh/ruff/pull/17946))
|
||||
- \[`airflow`\] Remove `airflow.utils.dag_parsing_context.get_parsing_context` (`AIR301`) ([#17852](https://github.com/astral-sh/ruff/pull/17852))
|
||||
- \[`airflow`\] Skip attribute check in try catch block (`AIR301`) ([#17790](https://github.com/astral-sh/ruff/pull/17790))
|
||||
- \[`flake8-bandit`\] Mark tuples of string literals as trusted input in `S603` ([#17801](https://github.com/astral-sh/ruff/pull/17801))
|
||||
- \[`isort`\] Check full module path against project root(s) when categorizing first-party imports ([#16565](https://github.com/astral-sh/ruff/pull/16565))
|
||||
- \[`ruff`\] Add new rule `in-empty-collection` (`RUF060`) ([#16480](https://github.com/astral-sh/ruff/pull/16480))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix missing `combine` call for `lint.typing-extensions` setting ([#17823](https://github.com/astral-sh/ruff/pull/17823))
|
||||
- \[`flake8-async`\] Fix module name in `ASYNC110`, `ASYNC115`, and `ASYNC116` fixes ([#17774](https://github.com/astral-sh/ruff/pull/17774))
|
||||
- \[`pyupgrade`\] Add spaces between tokens as necessary to avoid syntax errors in `UP018` autofix ([#17648](https://github.com/astral-sh/ruff/pull/17648))
|
||||
- \[`refurb`\] Fix false positive for float and complex numbers in `FURB116` ([#17661](https://github.com/astral-sh/ruff/pull/17661))
|
||||
- [parser] Flag single unparenthesized generator expr with trailing comma in arguments. ([#17893](https://github.com/astral-sh/ruff/pull/17893))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add instructions on how to upgrade to a newer Rust version ([#17928](https://github.com/astral-sh/ruff/pull/17928))
|
||||
- Update code of conduct email address ([#17875](https://github.com/astral-sh/ruff/pull/17875))
|
||||
- Add fix safety sections to `PLC2801`, `PLR1722`, and `RUF013` ([#17825](https://github.com/astral-sh/ruff/pull/17825), [#17826](https://github.com/astral-sh/ruff/pull/17826), [#17759](https://github.com/astral-sh/ruff/pull/17759))
|
||||
- Add link to `check-typed-exception` from `S110` and `S112` ([#17786](https://github.com/astral-sh/ruff/pull/17786))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Allow passing a virtual environment to `ruff analyze graph` ([#17743](https://github.com/astral-sh/ruff/pull/17743))
|
||||
|
||||
## 0.11.8
|
||||
|
||||
### Preview features
|
||||
|
||||
105
Cargo.lock
generated
105
Cargo.lock
generated
@@ -334,9 +334,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.37"
|
||||
version = "4.5.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
|
||||
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -344,9 +344,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.37"
|
||||
version = "4.5.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
|
||||
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -409,7 +409,7 @@ version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"nix 0.29.0",
|
||||
"terminfo",
|
||||
"thiserror 2.0.12",
|
||||
"which",
|
||||
@@ -681,11 +681,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.6"
|
||||
version = "3.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
|
||||
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"nix 0.30.1",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -904,7 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1057,9 +1057,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -1485,7 +1485,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1539,9 +1539,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.12"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd"
|
||||
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
@@ -1549,14 +1549,14 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.12"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300"
|
||||
checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1771,6 +1771,15 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb"
|
||||
dependencies = [
|
||||
"unicode-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@@ -1871,6 +1880,18 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -2449,7 +2470,7 @@ version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2549,7 +2570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2583,6 +2604,7 @@ dependencies = [
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_parser",
|
||||
@@ -2701,6 +2723,7 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"libcst",
|
||||
"markdown",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
"regex",
|
||||
@@ -2709,6 +2732,7 @@ dependencies = [
|
||||
"ruff_formatter",
|
||||
"ruff_linter",
|
||||
"ruff_notebook",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_codegen",
|
||||
"ruff_python_formatter",
|
||||
@@ -2725,7 +2749,9 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-indicatif",
|
||||
"tracing-subscriber",
|
||||
"ty",
|
||||
"ty_project",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2785,7 +2811,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -2813,6 +2839,7 @@ dependencies = [
|
||||
"regex",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
@@ -2874,6 +2901,13 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_options_metadata"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
@@ -3112,11 +3146,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"log",
|
||||
"ruff_formatter",
|
||||
@@ -3160,6 +3194,7 @@ dependencies = [
|
||||
"ruff_graph",
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_semantic",
|
||||
@@ -3206,7 +3241,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3219,7 +3254,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.3",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3237,7 +3272,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=f78a641d2086695ac0ef96cbe915bf80b5a690f2#f78a641d2086695ac0ef96cbe915bf80b5a690f2"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3260,12 +3295,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=f78a641d2086695ac0ef96cbe915bf80b5a690f2#f78a641d2086695ac0ef96cbe915bf80b5a690f2"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=f78a641d2086695ac0ef96cbe915bf80b5a690f2#f78a641d2086695ac0ef96cbe915bf80b5a690f2"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3602,10 +3637,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.0.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3961,6 +3996,7 @@ dependencies = [
|
||||
"crossbeam",
|
||||
"ctrlc",
|
||||
"filetime",
|
||||
"indicatif",
|
||||
"insta",
|
||||
"insta-cmd",
|
||||
"jiff",
|
||||
@@ -4013,6 +4049,7 @@ dependencies = [
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_macros",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_text_size",
|
||||
@@ -4143,7 +4180,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"log",
|
||||
"ruff_db",
|
||||
@@ -4219,6 +4256,12 @@ dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-id"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@@ -4322,7 +4365,7 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"rand 0.9.1",
|
||||
"uuid-macro-internal",
|
||||
|
||||
@@ -23,6 +23,7 @@ ruff_index = { path = "crates/ruff_index" }
|
||||
ruff_linter = { path = "crates/ruff_linter" }
|
||||
ruff_macros = { path = "crates/ruff_macros" }
|
||||
ruff_notebook = { path = "crates/ruff_notebook" }
|
||||
ruff_options_metadata = { path = "crates/ruff_options_metadata" }
|
||||
ruff_python_ast = { path = "crates/ruff_python_ast" }
|
||||
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
|
||||
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
|
||||
@@ -37,6 +38,7 @@ ruff_source_file = { path = "crates/ruff_source_file" }
|
||||
ruff_text_size = { path = "crates/ruff_text_size" }
|
||||
ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
ty = { path = "crates/ty" }
|
||||
ty_ide = { path = "crates/ty_ide" }
|
||||
ty_project = { path = "crates/ty_project", default-features = false }
|
||||
ty_python_semantic = { path = "crates/ty_python_semantic" }
|
||||
@@ -124,7 +126,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "f78a641d2086695ac0ef96cbe915bf80b5a690f2" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "7edce6e248f35c8114b4b021cdb474a3fb2813b3" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -182,7 +184,7 @@ wild = { version = "2" }
|
||||
zip = { version = "0.6.6", default-features = false }
|
||||
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["getrandom"]
|
||||
ignored = ["getrandom", "ruff_options_metadata"]
|
||||
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -149,8 +149,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.11.8/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.8/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.11.9/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.9/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,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.11.8
|
||||
rev: v0.11.9
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -20,6 +20,7 @@ ruff_graph = { workspace = true, features = ["serde", "clap"] }
|
||||
ruff_linter = { workspace = true, features = ["clap"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true, features = ["serde"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
@@ -83,7 +84,7 @@ dist = true
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
|
||||
tikv-jemallocator = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -22,12 +22,12 @@ use ruff_linter::settings::types::{
|
||||
PythonVersion, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
|
||||
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::{Options, PycodestyleOptions};
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
use rustc_hash::FxHashMap;
|
||||
use toml;
|
||||
|
||||
@@ -439,7 +439,7 @@ impl LintCacheData {
|
||||
.map(|msg| {
|
||||
// Make sure that all message use the same source file.
|
||||
assert_eq!(
|
||||
&msg.file,
|
||||
msg.file,
|
||||
messages.first().unwrap().source_file(),
|
||||
"message uses a different source file"
|
||||
);
|
||||
|
||||
@@ -2,10 +2,8 @@ use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
|
||||
use itertools::Itertools;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_workspace::{
|
||||
options::Options,
|
||||
options_base::{OptionField, OptionSet, OptionsMetadata, Visit},
|
||||
};
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ruff_workspace::options::Options;
|
||||
|
||||
#[derive(Default)]
|
||||
struct CollectOptionsVisitor {
|
||||
|
||||
@@ -2,8 +2,8 @@ use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::args::HelpFormat;
|
||||
|
||||
use ruff_options_metadata::OptionsMetadata;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
#[expect(clippy::print_stdout)]
|
||||
pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> {
|
||||
|
||||
@@ -15,7 +15,7 @@ use rustc_hash::FxHashMap;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_linter::codes::Rule;
|
||||
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource};
|
||||
use ruff_linter::message::{Message, SyntaxErrorMessage};
|
||||
use ruff_linter::message::Message;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::pyproject_toml::lint_pyproject_toml;
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
@@ -102,11 +102,7 @@ impl Diagnostics {
|
||||
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
|
||||
let dummy = SourceFileBuilder::new(name, "").finish();
|
||||
Self::new(
|
||||
vec![Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: err.to_string(),
|
||||
range: TextRange::default(),
|
||||
file: dummy,
|
||||
})],
|
||||
vec![Message::syntax_error(err, TextRange::default(), dummy)],
|
||||
FxHashMap::default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
not(target_os = "windows"),
|
||||
not(target_os = "openbsd"),
|
||||
not(target_os = "aix"),
|
||||
not(target_os = "android"),
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
|
||||
@@ -59,13 +59,40 @@ type KeyDiagnosticFields = (
|
||||
Severity,
|
||||
);
|
||||
|
||||
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[(
|
||||
DiagnosticId::lint("unused-ignore-comment"),
|
||||
Some("/src/tomllib/_parser.py"),
|
||||
Some(22299..22333),
|
||||
"Unused blanket `type: ignore` directive",
|
||||
Severity::Warning,
|
||||
)];
|
||||
// left: [
|
||||
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(8224..8254), "Argument to function `skip_until` is incorrect", Error),
|
||||
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(16914..16948), "Argument to function `skip_until` is incorrect", Error),
|
||||
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(17319..17363), "Argument to function `skip_until` is incorrect", Error),
|
||||
// ]
|
||||
//right: [
|
||||
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(8224..8254), "Argument to this function is incorrect", Error),
|
||||
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(16914..16948), "Argument to this function is incorrect", Error),
|
||||
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(17319..17363), "Argument to this function is incorrect", Error),
|
||||
// ]
|
||||
|
||||
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[
|
||||
(
|
||||
DiagnosticId::lint("invalid-argument-type"),
|
||||
Some("/src/tomllib/_parser.py"),
|
||||
Some(8224..8254),
|
||||
"Argument to function `skip_until` is incorrect",
|
||||
Severity::Error,
|
||||
),
|
||||
(
|
||||
DiagnosticId::lint("invalid-argument-type"),
|
||||
Some("/src/tomllib/_parser.py"),
|
||||
Some(16914..16948),
|
||||
"Argument to function `skip_until` is incorrect",
|
||||
Severity::Error,
|
||||
),
|
||||
(
|
||||
DiagnosticId::lint("invalid-argument-type"),
|
||||
Some("/src/tomllib/_parser.py"),
|
||||
Some(17319..17363),
|
||||
"Argument to function `skip_until` is incorrect",
|
||||
Severity::Error,
|
||||
),
|
||||
];
|
||||
|
||||
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
|
||||
SystemPathBuf::from("src").join(file.name())
|
||||
@@ -203,7 +230,7 @@ fn assert_diagnostics(db: &dyn Db, diagnostics: &[Diagnostic], expected: &[KeyDi
|
||||
diagnostic.id(),
|
||||
diagnostic
|
||||
.primary_span()
|
||||
.map(|span| span.file())
|
||||
.map(|span| span.expect_ty_file())
|
||||
.map(|file| file.path(db).as_str()),
|
||||
diagnostic
|
||||
.primary_span()
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::{fmt::Formatter, sync::Arc};
|
||||
|
||||
use thiserror::Error;
|
||||
use render::{FileResolver, Input};
|
||||
use ruff_source_file::{SourceCode, SourceFile};
|
||||
|
||||
use ruff_annotate_snippets::Level as AnnotateLevel;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
pub use self::render::DisplayDiagnostic;
|
||||
use crate::files::File;
|
||||
use crate::Db;
|
||||
use crate::{files::File, Db};
|
||||
|
||||
use self::render::FileResolver;
|
||||
mod render;
|
||||
mod stylesheet;
|
||||
|
||||
@@ -115,10 +114,9 @@ impl Diagnostic {
|
||||
/// callers should prefer using this with `write!` instead of `writeln!`.
|
||||
pub fn display<'a>(
|
||||
&'a self,
|
||||
db: &'a dyn Db,
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
) -> DisplayDiagnostic<'a> {
|
||||
let resolver = FileResolver::new(db);
|
||||
DisplayDiagnostic::new(resolver, config, self)
|
||||
}
|
||||
|
||||
@@ -233,6 +231,16 @@ impl Diagnostic {
|
||||
self.primary_annotation().map(|ann| ann.tags.as_slice())
|
||||
}
|
||||
|
||||
/// Returns the "primary" span of this diagnostic, panicking if it does not exist.
|
||||
///
|
||||
/// This should typically only be used when working with diagnostics in Ruff, where diagnostics
|
||||
/// are currently required to have a primary span.
|
||||
///
|
||||
/// See [`Diagnostic::primary_span`] for more details.
|
||||
pub fn expect_primary_span(&self) -> Span {
|
||||
self.primary_span().expect("Expected a primary span")
|
||||
}
|
||||
|
||||
/// Returns a key that can be used to sort two diagnostics into the canonical order
|
||||
/// in which they should appear when rendered.
|
||||
pub fn rendering_sort_key<'a>(&'a self, db: &'a dyn Db) -> impl Ord + 'a {
|
||||
@@ -267,11 +275,7 @@ impl Ord for RenderingSortKey<'_> {
|
||||
self.diagnostic.primary_span(),
|
||||
other.diagnostic.primary_span(),
|
||||
) {
|
||||
let order = span1
|
||||
.file()
|
||||
.path(self.db)
|
||||
.as_str()
|
||||
.cmp(span2.file().path(self.db).as_str());
|
||||
let order = span1.file().path(&self.db).cmp(span2.file().path(&self.db));
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
@@ -608,62 +612,84 @@ impl DiagnosticId {
|
||||
code.split_once(':').map(|(_, rest)| rest)
|
||||
}
|
||||
|
||||
/// Returns `true` if this `DiagnosticId` matches the given name.
|
||||
/// Returns a concise description of this diagnostic ID.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// use ruff_db::diagnostic::DiagnosticId;
|
||||
///
|
||||
/// assert!(DiagnosticId::Io.matches("io"));
|
||||
/// assert!(DiagnosticId::lint("test").matches("lint:test"));
|
||||
/// assert!(!DiagnosticId::lint("test").matches("test"));
|
||||
/// ```
|
||||
pub fn matches(&self, expected_name: &str) -> bool {
|
||||
match self.as_str() {
|
||||
Ok(id) => id == expected_name,
|
||||
Err(DiagnosticAsStrError::Category { category, name }) => expected_name
|
||||
.strip_prefix(category)
|
||||
.and_then(|prefix| prefix.strip_prefix(":"))
|
||||
.is_some_and(|rest| rest == name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Result<&str, DiagnosticAsStrError> {
|
||||
Ok(match self {
|
||||
/// Note that this doesn't include the lint's category. It
|
||||
/// only includes the lint's name.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
DiagnosticId::Panic => "panic",
|
||||
DiagnosticId::Io => "io",
|
||||
DiagnosticId::InvalidSyntax => "invalid-syntax",
|
||||
DiagnosticId::Lint(name) => {
|
||||
return Err(DiagnosticAsStrError::Category {
|
||||
category: "lint",
|
||||
name: name.as_str(),
|
||||
})
|
||||
}
|
||||
DiagnosticId::Lint(name) => name.as_str(),
|
||||
DiagnosticId::RevealedType => "revealed-type",
|
||||
DiagnosticId::UnknownRule => "unknown-rule",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
|
||||
pub enum DiagnosticAsStrError {
|
||||
/// The id can't be converted to a string because it belongs to a sub-category.
|
||||
#[error("id from a sub-category: {category}:{name}")]
|
||||
Category {
|
||||
/// The id's category.
|
||||
category: &'static str,
|
||||
/// The diagnostic id in this category.
|
||||
name: &'static str,
|
||||
},
|
||||
pub fn is_invalid_syntax(&self) -> bool {
|
||||
matches!(self, Self::InvalidSyntax)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DiagnosticId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self.as_str() {
|
||||
Ok(name) => f.write_str(name),
|
||||
Err(DiagnosticAsStrError::Category { category, name }) => {
|
||||
write!(f, "{category}:{name}")
|
||||
}
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A unified file representation for both ruff and ty.
|
||||
///
|
||||
/// Such a representation is needed for rendering [`Diagnostic`]s that can optionally contain
|
||||
/// [`Annotation`]s with [`Span`]s that need to refer to the text of a file. However, ty and ruff
|
||||
/// use very different file types: a `Copy`-able salsa-interned [`File`], and a heavier-weight
|
||||
/// [`SourceFile`], respectively.
|
||||
///
|
||||
/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
|
||||
/// emitting diagnostics from both ty and ruff.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum UnifiedFile {
|
||||
Ty(File),
|
||||
Ruff(SourceFile),
|
||||
}
|
||||
|
||||
impl UnifiedFile {
|
||||
pub fn path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a str {
|
||||
match self {
|
||||
UnifiedFile::Ty(file) => resolver.path(*file),
|
||||
UnifiedFile::Ruff(file) => file.name(),
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_source(&self, resolver: &dyn FileResolver) -> DiagnosticSource {
|
||||
match self {
|
||||
UnifiedFile::Ty(file) => DiagnosticSource::Ty(resolver.input(*file)),
|
||||
UnifiedFile::Ruff(file) => DiagnosticSource::Ruff(file.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A unified wrapper for types that can be converted to a [`SourceCode`].
|
||||
///
|
||||
/// As with [`UnifiedFile`], ruff and ty use slightly different representations for source code.
|
||||
/// [`DiagnosticSource`] wraps both of these and provides the single
|
||||
/// [`DiagnosticSource::as_source_code`] method to produce a [`SourceCode`] with the appropriate
|
||||
/// lifetimes.
|
||||
///
|
||||
/// See [`UnifiedFile::diagnostic_source`] for a way to obtain a [`DiagnosticSource`] from a file
|
||||
/// and [`FileResolver`].
|
||||
#[derive(Clone, Debug)]
|
||||
enum DiagnosticSource {
|
||||
Ty(Input),
|
||||
Ruff(SourceFile),
|
||||
}
|
||||
|
||||
impl DiagnosticSource {
|
||||
/// Returns this input as a `SourceCode` for convenient querying.
|
||||
fn as_source_code(&self) -> SourceCode {
|
||||
match self {
|
||||
DiagnosticSource::Ty(input) => SourceCode::new(input.text.as_str(), &input.line_index),
|
||||
DiagnosticSource::Ruff(source) => SourceCode::new(source.source_text(), source.index()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -675,14 +701,14 @@ impl std::fmt::Display for DiagnosticId {
|
||||
/// the entire file. For example, when the file should be executable but isn't.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Span {
|
||||
file: File,
|
||||
file: UnifiedFile,
|
||||
range: Option<TextRange>,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
/// Returns the `File` attached to this `Span`.
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
/// Returns the `UnifiedFile` attached to this `Span`.
|
||||
pub fn file(&self) -> &UnifiedFile {
|
||||
&self.file
|
||||
}
|
||||
|
||||
/// Returns the range, if available, attached to this `Span`.
|
||||
@@ -703,10 +729,38 @@ impl Span {
|
||||
pub fn with_optional_range(self, range: Option<TextRange>) -> Span {
|
||||
Span { range, ..self }
|
||||
}
|
||||
|
||||
/// Returns the [`File`] attached to this [`Span`].
|
||||
///
|
||||
/// Panics if the file is a [`UnifiedFile::Ruff`] instead of a [`UnifiedFile::Ty`].
|
||||
pub fn expect_ty_file(&self) -> File {
|
||||
match self.file {
|
||||
UnifiedFile::Ty(file) => file,
|
||||
UnifiedFile::Ruff(_) => panic!("Expected a ty `File`, found a ruff `SourceFile`"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`SourceFile`] attached to this [`Span`].
|
||||
///
|
||||
/// Panics if the file is a [`UnifiedFile::Ty`] instead of a [`UnifiedFile::Ruff`].
|
||||
pub fn expect_ruff_file(&self) -> &SourceFile {
|
||||
match &self.file {
|
||||
UnifiedFile::Ty(_) => panic!("Expected a ruff `SourceFile`, found a ty `File`"),
|
||||
UnifiedFile::Ruff(file) => file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for Span {
|
||||
fn from(file: File) -> Span {
|
||||
let file = UnifiedFile::Ty(file);
|
||||
Span { file, range: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SourceFile> for Span {
|
||||
fn from(file: SourceFile) -> Self {
|
||||
let file = UnifiedFile::Ruff(file);
|
||||
Span { file, range: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, Severity, SubDiagnostic,
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
|
||||
SubDiagnostic,
|
||||
};
|
||||
|
||||
/// A type that implements `std::fmt::Display` for diagnostic rendering.
|
||||
@@ -30,17 +31,16 @@ use super::{
|
||||
/// values. When using Salsa, this most commonly corresponds to the lifetime
|
||||
/// of a Salsa `Db`.
|
||||
/// * The lifetime of the diagnostic being rendered.
|
||||
#[derive(Debug)]
|
||||
pub struct DisplayDiagnostic<'a> {
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
resolver: FileResolver<'a>,
|
||||
resolver: &'a dyn FileResolver,
|
||||
annotate_renderer: AnnotateRenderer,
|
||||
diag: &'a Diagnostic,
|
||||
}
|
||||
|
||||
impl<'a> DisplayDiagnostic<'a> {
|
||||
pub(crate) fn new(
|
||||
resolver: FileResolver<'a>,
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
diag: &'a Diagnostic,
|
||||
) -> DisplayDiagnostic<'a> {
|
||||
@@ -86,11 +86,13 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
write!(
|
||||
f,
|
||||
" {path}",
|
||||
path = fmt_styled(self.resolver.path(span.file()), stylesheet.emphasis)
|
||||
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let input = self.resolver.input(span.file());
|
||||
let start = input.as_source_code().line_column(range.start());
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
write!(
|
||||
f,
|
||||
@@ -115,7 +117,7 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
.emphasis(stylesheet.emphasis)
|
||||
.none(stylesheet.none);
|
||||
|
||||
let resolved = Resolved::new(&self.resolver, self.diag);
|
||||
let resolved = Resolved::new(self.resolver, self.diag);
|
||||
let renderable = resolved.to_renderable(self.config.context);
|
||||
for diag in renderable.diagnostics.iter() {
|
||||
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
|
||||
@@ -138,26 +140,23 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
/// both.)
|
||||
#[derive(Debug)]
|
||||
struct Resolved<'a> {
|
||||
id: String,
|
||||
diagnostics: Vec<ResolvedDiagnostic<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Resolved<'a> {
|
||||
/// Creates a new resolved set of diagnostics.
|
||||
fn new(resolver: &FileResolver<'a>, diag: &'a Diagnostic) -> Resolved<'a> {
|
||||
fn new(resolver: &'a dyn FileResolver, diag: &'a Diagnostic) -> Resolved<'a> {
|
||||
let mut diagnostics = vec![];
|
||||
diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, diag));
|
||||
for sub in &diag.inner.subs {
|
||||
diagnostics.push(ResolvedDiagnostic::from_sub_diagnostic(resolver, sub));
|
||||
}
|
||||
let id = diag.inner.id.to_string();
|
||||
Resolved { id, diagnostics }
|
||||
Resolved { diagnostics }
|
||||
}
|
||||
|
||||
/// Creates a value that is amenable to rendering directly.
|
||||
fn to_renderable(&self, context: usize) -> Renderable<'_> {
|
||||
Renderable {
|
||||
id: &self.id,
|
||||
diagnostics: self
|
||||
.diagnostics
|
||||
.iter()
|
||||
@@ -175,6 +174,7 @@ impl<'a> Resolved<'a> {
|
||||
#[derive(Debug)]
|
||||
struct ResolvedDiagnostic<'a> {
|
||||
severity: Severity,
|
||||
id: Option<String>,
|
||||
message: String,
|
||||
annotations: Vec<ResolvedAnnotation<'a>>,
|
||||
}
|
||||
@@ -182,7 +182,7 @@ struct ResolvedDiagnostic<'a> {
|
||||
impl<'a> ResolvedDiagnostic<'a> {
|
||||
/// Resolve a single diagnostic.
|
||||
fn from_diagnostic(
|
||||
resolver: &FileResolver<'a>,
|
||||
resolver: &'a dyn FileResolver,
|
||||
diag: &'a Diagnostic,
|
||||
) -> ResolvedDiagnostic<'a> {
|
||||
let annotations: Vec<_> = diag
|
||||
@@ -190,25 +190,16 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.annotations
|
||||
.iter()
|
||||
.filter_map(|ann| {
|
||||
let path = resolver.path(ann.span.file);
|
||||
let input = resolver.input(ann.span.file);
|
||||
ResolvedAnnotation::new(path, &input, ann)
|
||||
let path = ann.span.file.path(resolver);
|
||||
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
|
||||
ResolvedAnnotation::new(path, &diagnostic_source, ann)
|
||||
})
|
||||
.collect();
|
||||
let message = if diag.inner.message.as_str().is_empty() {
|
||||
diag.inner.id.to_string()
|
||||
} else {
|
||||
// TODO: See the comment on `Renderable::id` for
|
||||
// a plausible better idea than smushing the ID
|
||||
// into the diagnostic message.
|
||||
format!(
|
||||
"{id}: {message}",
|
||||
id = diag.inner.id,
|
||||
message = diag.inner.message.as_str(),
|
||||
)
|
||||
};
|
||||
let id = Some(diag.inner.id.to_string());
|
||||
let message = diag.inner.message.as_str().to_string();
|
||||
ResolvedDiagnostic {
|
||||
severity: diag.inner.severity,
|
||||
id,
|
||||
message,
|
||||
annotations,
|
||||
}
|
||||
@@ -216,7 +207,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
|
||||
/// Resolve a single sub-diagnostic.
|
||||
fn from_sub_diagnostic(
|
||||
resolver: &FileResolver<'a>,
|
||||
resolver: &'a dyn FileResolver,
|
||||
diag: &'a SubDiagnostic,
|
||||
) -> ResolvedDiagnostic<'a> {
|
||||
let annotations: Vec<_> = diag
|
||||
@@ -224,13 +215,14 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.annotations
|
||||
.iter()
|
||||
.filter_map(|ann| {
|
||||
let path = resolver.path(ann.span.file);
|
||||
let input = resolver.input(ann.span.file);
|
||||
ResolvedAnnotation::new(path, &input, ann)
|
||||
let path = ann.span.file.path(resolver);
|
||||
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
|
||||
ResolvedAnnotation::new(path, &diagnostic_source, ann)
|
||||
})
|
||||
.collect();
|
||||
ResolvedDiagnostic {
|
||||
severity: diag.inner.severity,
|
||||
id: None,
|
||||
message: diag.inner.message.as_str().to_string(),
|
||||
annotations,
|
||||
}
|
||||
@@ -259,10 +251,18 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
continue;
|
||||
};
|
||||
|
||||
let prev_context_ends =
|
||||
context_after(&prev.input.as_source_code(), context, prev.line_end).get();
|
||||
let this_context_begins =
|
||||
context_before(&ann.input.as_source_code(), context, ann.line_start).get();
|
||||
let prev_context_ends = context_after(
|
||||
&prev.diagnostic_source.as_source_code(),
|
||||
context,
|
||||
prev.line_end,
|
||||
)
|
||||
.get();
|
||||
let this_context_begins = context_before(
|
||||
&ann.diagnostic_source.as_source_code(),
|
||||
context,
|
||||
ann.line_start,
|
||||
)
|
||||
.get();
|
||||
// The boundary case here is when `prev_context_ends`
|
||||
// is exactly one less than `this_context_begins`. In
|
||||
// that case, the context windows are adajcent and we
|
||||
@@ -289,6 +289,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
|
||||
RenderableDiagnostic {
|
||||
severity: self.severity,
|
||||
id: self.id.as_deref(),
|
||||
message: &self.message,
|
||||
snippets_by_input,
|
||||
}
|
||||
@@ -304,7 +305,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
#[derive(Debug)]
|
||||
struct ResolvedAnnotation<'a> {
|
||||
path: &'a str,
|
||||
input: Input,
|
||||
diagnostic_source: DiagnosticSource,
|
||||
range: TextRange,
|
||||
line_start: OneIndexed,
|
||||
line_end: OneIndexed,
|
||||
@@ -318,8 +319,12 @@ impl<'a> ResolvedAnnotation<'a> {
|
||||
/// `path` is the path of the file that this annotation points to.
|
||||
///
|
||||
/// `input` is the contents of the file that this annotation points to.
|
||||
fn new(path: &'a str, input: &Input, ann: &'a Annotation) -> Option<ResolvedAnnotation<'a>> {
|
||||
let source = input.as_source_code();
|
||||
fn new(
|
||||
path: &'a str,
|
||||
diagnostic_source: &DiagnosticSource,
|
||||
ann: &'a Annotation,
|
||||
) -> Option<ResolvedAnnotation<'a>> {
|
||||
let source = diagnostic_source.as_source_code();
|
||||
let (range, line_start, line_end) = match (ann.span.range(), ann.message.is_some()) {
|
||||
// An annotation with no range AND no message is probably(?)
|
||||
// meaningless, but we should try to render it anyway.
|
||||
@@ -345,7 +350,7 @@ impl<'a> ResolvedAnnotation<'a> {
|
||||
};
|
||||
Some(ResolvedAnnotation {
|
||||
path,
|
||||
input: input.clone(),
|
||||
diagnostic_source: diagnostic_source.clone(),
|
||||
range,
|
||||
line_start,
|
||||
line_end,
|
||||
@@ -364,20 +369,6 @@ impl<'a> ResolvedAnnotation<'a> {
|
||||
/// renderable value. This is usually the lifetime of `Resolved`.
|
||||
#[derive(Debug)]
|
||||
struct Renderable<'r> {
|
||||
// TODO: This is currently unused in the rendering logic below. I'm not
|
||||
// 100% sure yet where I want to put it, but I like what `rustc` does:
|
||||
//
|
||||
// error[E0599]: no method named `sub_builder` <..snip..>
|
||||
//
|
||||
// I believe in order to do this, we'll need to patch it in to
|
||||
// `ruff_annotate_snippets` though. We leave it here for now with that plan
|
||||
// in mind.
|
||||
//
|
||||
// (At time of writing, 2025-03-13, we currently render the diagnostic
|
||||
// ID into the main message of the parent diagnostic. We don't use this
|
||||
// specific field to do that though.)
|
||||
#[expect(dead_code)]
|
||||
id: &'r str,
|
||||
diagnostics: Vec<RenderableDiagnostic<'r>>,
|
||||
}
|
||||
|
||||
@@ -386,6 +377,12 @@ struct Renderable<'r> {
|
||||
struct RenderableDiagnostic<'r> {
|
||||
/// The severity of the diagnostic.
|
||||
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.
|
||||
///
|
||||
/// An ID is always present for top-level diagnostics and always absent for
|
||||
/// sub-diagnostics.
|
||||
id: Option<&'r str>,
|
||||
/// The message emitted with the diagnostic, before any snippets are
|
||||
/// rendered.
|
||||
message: &'r str,
|
||||
@@ -406,7 +403,11 @@ impl RenderableDiagnostic<'_> {
|
||||
.iter()
|
||||
.map(|snippet| snippet.to_annotate(path))
|
||||
});
|
||||
level.title(self.message).snippets(snippets)
|
||||
let mut message = level.title(self.message);
|
||||
if let Some(id) = self.id {
|
||||
message = message.id(id);
|
||||
}
|
||||
message.snippets(snippets)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,8 +511,8 @@ impl<'r> RenderableSnippet<'r> {
|
||||
!anns.is_empty(),
|
||||
"creating a renderable snippet requires a non-zero number of annotations",
|
||||
);
|
||||
let input = &anns[0].input;
|
||||
let source = input.as_source_code();
|
||||
let diagnostic_source = &anns[0].diagnostic_source;
|
||||
let source = diagnostic_source.as_source_code();
|
||||
let has_primary = anns.iter().any(|ann| ann.is_primary);
|
||||
|
||||
let line_start = context_before(
|
||||
@@ -527,7 +528,7 @@ impl<'r> RenderableSnippet<'r> {
|
||||
|
||||
let snippet_start = source.line_start(line_start);
|
||||
let snippet_end = source.line_end(line_end);
|
||||
let snippet = input
|
||||
let snippet = diagnostic_source
|
||||
.as_source_code()
|
||||
.slice(TextRange::new(snippet_start, snippet_end));
|
||||
|
||||
@@ -613,7 +614,7 @@ impl<'r> RenderableAnnotation<'r> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that facilitates the retrieval of source code from a `Span`.
|
||||
/// A trait that facilitates the retrieval of source code from a `Span`.
|
||||
///
|
||||
/// At present, this is tightly coupled with a Salsa database. In the future,
|
||||
/// it is intended for this resolver to become an abstraction providing a
|
||||
@@ -628,36 +629,24 @@ impl<'r> RenderableAnnotation<'r> {
|
||||
/// callers will need to pass in a different "resolver" for turning `Span`s
|
||||
/// into actual file paths/contents. The infrastructure for this isn't fully in
|
||||
/// place, but this type serves to demarcate the intended abstraction boundary.
|
||||
pub(crate) struct FileResolver<'a> {
|
||||
db: &'a dyn Db,
|
||||
}
|
||||
|
||||
impl<'a> FileResolver<'a> {
|
||||
/// Creates a new resolver from a Salsa database.
|
||||
pub(crate) fn new(db: &'a dyn Db) -> FileResolver<'a> {
|
||||
FileResolver { db }
|
||||
}
|
||||
|
||||
pub trait FileResolver {
|
||||
/// Returns the path associated with the file given.
|
||||
fn path(&self, file: File) -> &'a str {
|
||||
relativize_path(
|
||||
self.db.system().current_directory(),
|
||||
file.path(self.db).as_str(),
|
||||
)
|
||||
}
|
||||
fn path(&self, file: File) -> &str;
|
||||
|
||||
/// Returns the input contents associated with the file given.
|
||||
fn input(&self, file: File) -> Input {
|
||||
Input {
|
||||
text: source_text(self.db, file),
|
||||
line_index: line_index(self.db, file),
|
||||
}
|
||||
}
|
||||
fn input(&self, file: File) -> Input;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FileResolver<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "<salsa based file resolver>")
|
||||
impl FileResolver for &dyn Db {
|
||||
fn path(&self, file: File) -> &str {
|
||||
relativize_path(self.system().current_directory(), file.path(*self).as_str())
|
||||
}
|
||||
|
||||
fn input(&self, file: File) -> Input {
|
||||
Input {
|
||||
text: source_text(*self, file),
|
||||
line_index: line_index(*self, file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,16 +656,9 @@ impl std::fmt::Debug for FileResolver<'_> {
|
||||
/// This contains the actual content of that input as well as a
|
||||
/// line index for efficiently querying its contents.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Input {
|
||||
text: SourceText,
|
||||
line_index: LineIndex,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// Returns this input as a `SourceCode` for convenient querying.
|
||||
fn as_source_code(&self) -> SourceCode<'_, '_> {
|
||||
SourceCode::new(self.text.as_str(), &self.line_index)
|
||||
}
|
||||
pub struct Input {
|
||||
pub(crate) text: SourceText,
|
||||
pub(crate) line_index: LineIndex,
|
||||
}
|
||||
|
||||
/// Returns the line number accounting for the given `len`
|
||||
@@ -730,6 +712,7 @@ mod tests {
|
||||
use crate::files::system_path_to_file;
|
||||
use crate::system::{DbWithWritableSystem, SystemPath};
|
||||
use crate::tests::TestDb;
|
||||
use crate::Upcast;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -803,7 +786,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -827,7 +810,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
warning: lint:test-diagnostic: main diagnostic message
|
||||
warning[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -847,7 +830,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
info: lint:test-diagnostic: main diagnostic message
|
||||
info[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -874,7 +857,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -893,7 +876,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -914,7 +897,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> non-ascii:5:1
|
||||
|
|
||||
3 | ΔΔΔΔΔΔΔΔΔΔΔΔ
|
||||
@@ -933,7 +916,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> non-ascii:2:2
|
||||
|
|
||||
1 | ☃☃☃☃☃☃☃☃☃☃☃☃
|
||||
@@ -957,7 +940,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
4 | dog
|
||||
@@ -974,7 +957,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
5 | elephant
|
||||
@@ -989,7 +972,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1006,7 +989,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:11:1
|
||||
|
|
||||
9 | inchworm
|
||||
@@ -1023,7 +1006,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1056,7 +1039,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1100,7 +1083,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1125,7 +1108,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1153,7 +1136,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1181,7 +1164,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1206,7 +1189,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1237,7 +1220,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:1:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1275,7 +1258,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> spacey-animals:8:1
|
||||
|
|
||||
7 | dog
|
||||
@@ -1292,7 +1275,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> spacey-animals:12:1
|
||||
|
|
||||
11 | gorilla
|
||||
@@ -1310,7 +1293,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> spacey-animals:13:1
|
||||
|
|
||||
11 | gorilla
|
||||
@@ -1350,7 +1333,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> spacey-animals:3:1
|
||||
|
|
||||
3 | beetle
|
||||
@@ -1379,7 +1362,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:3:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1416,7 +1399,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:3:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1453,7 +1436,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:3:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1481,7 +1464,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:3:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1517,7 +1500,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:3:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1556,7 +1539,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:3:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1604,7 +1587,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:3:1
|
||||
|
|
||||
1 | aardvark
|
||||
@@ -1640,7 +1623,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -1663,7 +1646,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -1683,7 +1666,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -1703,7 +1686,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:4
|
||||
|
|
||||
3 | canary
|
||||
@@ -1725,7 +1708,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:4
|
||||
|
|
||||
3 | canary
|
||||
@@ -1757,7 +1740,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:4:1
|
||||
|
|
||||
2 | beetle
|
||||
@@ -1786,7 +1769,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:4:1
|
||||
|
|
||||
2 | beetle
|
||||
@@ -1817,7 +1800,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -1852,7 +1835,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -1880,7 +1863,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
3 | canary
|
||||
@@ -1912,7 +1895,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:3
|
||||
|
|
||||
3 | canary
|
||||
@@ -1934,7 +1917,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:3
|
||||
|
|
||||
3 | canary
|
||||
@@ -1967,7 +1950,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:8:1
|
||||
|
|
||||
6 | finch
|
||||
@@ -2007,7 +1990,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:5:1
|
||||
|
|
||||
5 | elephant
|
||||
@@ -2051,7 +2034,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> fruits:1:1
|
||||
|
|
||||
1 | apple
|
||||
@@ -2086,7 +2069,7 @@ watermelon
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r"
|
||||
error: lint:test-diagnostic: main diagnostic message
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> animals:11:1
|
||||
|
|
||||
11 | kangaroo
|
||||
@@ -2174,8 +2157,9 @@ watermelon
|
||||
fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span {
|
||||
let span = self.path(path);
|
||||
|
||||
let text = source_text(&self.db, span.file());
|
||||
let line_index = line_index(&self.db, span.file());
|
||||
let file = span.expect_ty_file();
|
||||
let text = source_text(&self.db, file);
|
||||
let line_index = line_index(&self.db, file);
|
||||
let source = SourceCode::new(text.as_str(), &line_index);
|
||||
|
||||
let (line_start, offset_start) = parse_line_offset(line_offset_start);
|
||||
@@ -2237,7 +2221,7 @@ watermelon
|
||||
///
|
||||
/// (This will set the "printed" flag on `Diagnostic`.)
|
||||
fn render(&self, diag: &Diagnostic) -> String {
|
||||
diag.display(&self.db, &self.config).to_string()
|
||||
diag.display(&self.db.upcast(), &self.config).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ impl std::panic::RefUnwindSafe for Files {}
|
||||
#[salsa::input]
|
||||
pub struct File {
|
||||
/// The path of the file (immutable).
|
||||
#[return_ref]
|
||||
#[returns(ref)]
|
||||
pub path: FilePath,
|
||||
|
||||
/// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows
|
||||
|
||||
@@ -19,8 +19,8 @@ use crate::Db;
|
||||
#[salsa::input(debug)]
|
||||
pub struct FileRoot {
|
||||
/// The path of a root is guaranteed to never change.
|
||||
#[return_ref]
|
||||
path_buf: SystemPathBuf,
|
||||
#[returns(deref)]
|
||||
pub path: SystemPathBuf,
|
||||
|
||||
/// The kind of the root at the time of its creation.
|
||||
kind_at_time_of_creation: FileRootKind,
|
||||
@@ -32,10 +32,6 @@ pub struct FileRoot {
|
||||
}
|
||||
|
||||
impl FileRoot {
|
||||
pub fn path(self, db: &dyn Db) -> &SystemPath {
|
||||
self.path_buf(db)
|
||||
}
|
||||
|
||||
pub fn durability(self, db: &dyn Db) -> salsa::Durability {
|
||||
self.kind_at_time_of_creation(db).durability()
|
||||
}
|
||||
|
||||
@@ -61,13 +61,15 @@ pub fn max_parallelism() -> NonZeroUsize {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::files::Files;
|
||||
use crate::system::TestSystem;
|
||||
use crate::system::{DbWithTestSystem, System};
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
use crate::Db;
|
||||
use crate::{Db, Upcast};
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
/// Database that can be used for testing.
|
||||
///
|
||||
@@ -79,36 +81,37 @@ mod tests {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Events,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let events = Events::default();
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
tracing::trace!("event: {:?}", event);
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: VendoredFileSystem::default(),
|
||||
events: std::sync::Arc::default(),
|
||||
events,
|
||||
files: Files::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Empties the internal store of salsa events that have been emitted,
|
||||
/// and returns them as a `Vec` (equivalent to [`std::mem::take`]).
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are pending database snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events)
|
||||
.expect("expected no pending salsa database snapshots.");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
std::mem::take(inner.get_mut().unwrap())
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the emitted salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are pending database snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
@@ -133,7 +136,16 @@ mod tests {
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
ruff_python_ast::PythonVersion::latest()
|
||||
ruff_python_ast::PythonVersion::latest_ty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn Db> for TestDb {
|
||||
fn upcast(&self) -> &(dyn Db + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn Db + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,12 +160,5 @@ mod tests {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {:?}", event);
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::Db;
|
||||
/// reflected in the changed AST offsets.
|
||||
/// The other reason is that Ruff's AST doesn't implement `Eq` which Sala requires
|
||||
/// for determining if a query result is unchanged.
|
||||
#[salsa::tracked(return_ref, no_eq)]
|
||||
#[salsa::tracked(returns(ref), no_eq)]
|
||||
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
|
||||
let _span = tracing::trace_span!("parsed_module", ?file).entered();
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
ty = { workspace = true }
|
||||
ty_project = { workspace = true, features = ["schemars"] }
|
||||
ruff = { workspace = true }
|
||||
ruff_diagnostics = { workspace = true }
|
||||
ruff_formatter = { workspace = true }
|
||||
ruff_linter = { workspace = true, features = ["schemars"] }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_codegen = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
@@ -31,6 +33,7 @@ imara-diff = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
libcst = { workspace = true }
|
||||
markdown = { version = "1.0.0" }
|
||||
pretty_assertions = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
@@ -44,6 +47,7 @@ toml = { workspace = true, features = ["parse"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-indicatif = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
url = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = { workspace = true }
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_ty_schema};
|
||||
use crate::{
|
||||
generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference,
|
||||
generate_ty_options, generate_ty_rules, generate_ty_schema,
|
||||
};
|
||||
|
||||
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
|
||||
|
||||
@@ -38,5 +41,8 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
generate_docs::main(&generate_docs::Args {
|
||||
dry_run: args.mode.is_dry_run(),
|
||||
})?;
|
||||
generate_ty_options::main(&generate_ty_options::Args { mode: args.mode })?;
|
||||
generate_ty_rules::main(&generate_ty_rules::Args { mode: args.mode })?;
|
||||
generate_ty_cli_reference::main(&generate_ty_cli_reference::Args { mode: args.mode })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Generate CLI help.
|
||||
#![allow(clippy::print_stdout)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, str};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Generate Markdown documentation for applicable rules.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write as _;
|
||||
@@ -13,8 +12,8 @@ use strum::IntoEnumIterator;
|
||||
|
||||
use ruff_diagnostics::FixAvailability;
|
||||
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
|
||||
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
|
||||
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
use itertools::Itertools;
|
||||
use std::fmt::Write;
|
||||
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ruff_python_trivia::textwrap;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
|
||||
pub(crate) fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
@@ -11,8 +11,8 @@ use strum::IntoEnumIterator;
|
||||
use ruff_diagnostics::FixAvailability;
|
||||
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
|
||||
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
|
||||
use ruff_options_metadata::OptionsMetadata;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
const FIX_SYMBOL: &str = "🛠️";
|
||||
const PREVIEW_SYMBOL: &str = "🧪";
|
||||
|
||||
334
crates/ruff_dev/src/generate_ty_cli_reference.rs
Normal file
334
crates/ruff_dev/src/generate_ty_cli_reference.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
//! Generate a Markdown-compatible reference for the ty command-line interface.
|
||||
use std::cmp::max;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Command, CommandFactory};
|
||||
use itertools::Itertools;
|
||||
use pretty_assertions::StrComparison;
|
||||
|
||||
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
use ty::Cli;
|
||||
|
||||
const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"];
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
let reference_string = generate();
|
||||
let filename = "crates/ty/docs/cli.md";
|
||||
let reference_path = PathBuf::from(ROOT_DIR).join(filename);
|
||||
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{reference_string}");
|
||||
}
|
||||
Mode::Check => {
|
||||
match std::fs::read_to_string(reference_path) {
|
||||
Ok(current) => {
|
||||
if current == reference_string {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &reference_string);
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
bail!("{filename} not found, please run `{REGENERATE_ALL_COMMAND}`");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Write => match std::fs::read_to_string(&reference_path) {
|
||||
Ok(current) => {
|
||||
if current == reference_string {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
println!("Updating: {filename}");
|
||||
std::fs::write(reference_path, reference_string.as_bytes())?;
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
println!("Updating: {filename}");
|
||||
std::fs::write(reference_path, reference_string.as_bytes())?;
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
let mut ty = Cli::command();
|
||||
|
||||
// It is very important to build the command before beginning inspection or subcommands
|
||||
// will be missing all of the propagated options.
|
||||
ty.build();
|
||||
|
||||
let mut parents = Vec::new();
|
||||
|
||||
output.push_str("# CLI Reference\n\n");
|
||||
generate_command(&mut output, &ty, &mut parents);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[allow(clippy::format_push_string)]
|
||||
fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut Vec<&'a Command>) {
|
||||
if command.is_hide_set() && !SHOW_HIDDEN_COMMANDS.contains(&command.get_name()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate the command header.
|
||||
let name = if parents.is_empty() {
|
||||
command.get_name().to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{} {}",
|
||||
parents.iter().map(|cmd| cmd.get_name()).join(" "),
|
||||
command.get_name()
|
||||
)
|
||||
};
|
||||
|
||||
// Display the top-level `ty` command at the same level as its children
|
||||
let level = max(2, parents.len() + 1);
|
||||
output.push_str(&format!("{} {name}\n\n", "#".repeat(level)));
|
||||
|
||||
// Display the command description.
|
||||
if let Some(about) = command.get_long_about().or_else(|| command.get_about()) {
|
||||
output.push_str(&about.to_string());
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
// Display the usage
|
||||
{
|
||||
// This appears to be the simplest way to get rendered usage from Clap,
|
||||
// it is complicated to render it manually. It's annoying that it
|
||||
// requires a mutable reference but it doesn't really matter.
|
||||
let mut command = command.clone();
|
||||
output.push_str("<h3 class=\"cli-reference\">Usage</h3>\n\n");
|
||||
output.push_str(&format!(
|
||||
"```\n{}\n```",
|
||||
command
|
||||
.render_usage()
|
||||
.to_string()
|
||||
.trim_start_matches("Usage: "),
|
||||
));
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
if command.get_name() == "help" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a list of child commands
|
||||
let mut subcommands = command.get_subcommands().peekable();
|
||||
let has_subcommands = subcommands.peek().is_some();
|
||||
if has_subcommands {
|
||||
output.push_str("<h3 class=\"cli-reference\">Commands</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
|
||||
for subcommand in subcommands {
|
||||
if subcommand.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
let subcommand_name = format!("{name} {}", subcommand.get_name());
|
||||
output.push_str(&format!(
|
||||
"<dt><a href=\"#{}\"><code>{subcommand_name}</code></a></dt>",
|
||||
subcommand_name.replace(' ', "-")
|
||||
));
|
||||
if let Some(about) = subcommand.get_about() {
|
||||
output.push_str(&format!(
|
||||
"<dd>{}</dd>\n",
|
||||
markdown::to_html(&about.to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>\n\n");
|
||||
}
|
||||
|
||||
// Do not display options for commands with children
|
||||
if !has_subcommands {
|
||||
let name_key = name.replace(' ', "-");
|
||||
|
||||
// Display positional arguments
|
||||
let mut arguments = command
|
||||
.get_positionals()
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.peekable();
|
||||
|
||||
if arguments.peek().is_some() {
|
||||
output.push_str("<h3 class=\"cli-reference\">Arguments</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
|
||||
for arg in arguments {
|
||||
let id = format!("{name_key}--{}", arg.get_id());
|
||||
output.push_str(&format!("<dt id=\"{id}\">"));
|
||||
output.push_str(&format!(
|
||||
"<a href=\"#{id}\"<code>{}</code></a>",
|
||||
arg.get_id().to_string().to_uppercase(),
|
||||
));
|
||||
output.push_str("</dt>");
|
||||
if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) {
|
||||
output.push_str("<dd>");
|
||||
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
|
||||
output.push_str("</dd>");
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>\n\n");
|
||||
}
|
||||
|
||||
// Display options and flags
|
||||
let mut options = command
|
||||
.get_arguments()
|
||||
.filter(|arg| !arg.is_positional())
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.sorted_by_key(|arg| arg.get_id())
|
||||
.peekable();
|
||||
|
||||
if options.peek().is_some() {
|
||||
output.push_str("<h3 class=\"cli-reference\">Options</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
for opt in options {
|
||||
let Some(long) = opt.get_long() else { continue };
|
||||
let id = format!("{name_key}--{long}");
|
||||
|
||||
output.push_str(&format!("<dt id=\"{id}\">"));
|
||||
output.push_str(&format!("<a href=\"#{id}\"><code>--{long}</code></a>"));
|
||||
for long_alias in opt.get_all_aliases().into_iter().flatten() {
|
||||
output.push_str(&format!(", <code>--{long_alias}</code>"));
|
||||
}
|
||||
if let Some(short) = opt.get_short() {
|
||||
output.push_str(&format!(", <code>-{short}</code>"));
|
||||
}
|
||||
for short_alias in opt.get_all_short_aliases().into_iter().flatten() {
|
||||
output.push_str(&format!(", <code>-{short_alias}</code>"));
|
||||
}
|
||||
|
||||
// Re-implements private `Arg::is_takes_value_set` used in `Command::get_opts`
|
||||
if opt
|
||||
.get_num_args()
|
||||
.unwrap_or_else(|| 1.into())
|
||||
.takes_values()
|
||||
{
|
||||
if let Some(values) = opt.get_value_names() {
|
||||
for value in values {
|
||||
output.push_str(&format!(
|
||||
" <i>{}</i>",
|
||||
value.to_lowercase().replace('_', "-")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push_str("</dt>");
|
||||
if let Some(help) = opt.get_long_help().or_else(|| opt.get_help()) {
|
||||
output.push_str("<dd>");
|
||||
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
|
||||
emit_env_option(opt, output);
|
||||
emit_default_option(opt, output);
|
||||
emit_possible_options(opt, output);
|
||||
output.push_str("</dd>");
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>");
|
||||
}
|
||||
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
parents.push(command);
|
||||
|
||||
// Recurse to all of the subcommands.
|
||||
for subcommand in command.get_subcommands() {
|
||||
generate_command(output, subcommand, parents);
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
}
|
||||
|
||||
fn emit_env_option(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_env_set() {
|
||||
return;
|
||||
}
|
||||
if let Some(env) = opt.get_env() {
|
||||
output.push_str(&markdown::to_html(&format!(
|
||||
"May also be set with the `{}` environment variable.",
|
||||
env.to_string_lossy()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_default_option(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_default_value_set() || !opt.get_num_args().expect("built").takes_values() {
|
||||
return;
|
||||
}
|
||||
|
||||
let values = opt.get_default_values();
|
||||
if !values.is_empty() {
|
||||
let value = format!(
|
||||
"\n[default: {}]",
|
||||
opt.get_default_values()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy())
|
||||
.join(",")
|
||||
);
|
||||
output.push_str(&markdown::to_html(&value));
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_possible_options(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_possible_values_set() {
|
||||
return;
|
||||
}
|
||||
|
||||
let values = opt.get_possible_values();
|
||||
if !values.is_empty() {
|
||||
let value = format!(
|
||||
"\nPossible values:\n{}",
|
||||
values
|
||||
.into_iter()
|
||||
.filter(|value| !value.is_hide_set())
|
||||
.map(|value| {
|
||||
let name = value.get_name();
|
||||
value.get_help().map_or_else(
|
||||
|| format!(" - `{name}`"),
|
||||
|help| format!(" - `{name}`: {help}"),
|
||||
)
|
||||
})
|
||||
.collect_vec()
|
||||
.join("\n"),
|
||||
);
|
||||
output.push_str(&markdown::to_html(&value));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{main, Args};
|
||||
|
||||
#[test]
|
||||
fn ty_cli_reference_is_up_to_date() -> Result<()> {
|
||||
main(&Args { mode: Mode::Check })
|
||||
}
|
||||
}
|
||||
257
crates/ruff_dev/src/generate_ty_options.rs
Normal file
257
crates/ruff_dev/src/generate_ty_options.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
|
||||
|
||||
use anyhow::bail;
|
||||
use itertools::Itertools;
|
||||
use pretty_assertions::StrComparison;
|
||||
use std::{fmt::Write, path::PathBuf};
|
||||
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ty_project::metadata::Options;
|
||||
|
||||
use crate::{
|
||||
generate_all::{Mode, REGENERATE_ALL_COMMAND},
|
||||
ROOT_DIR,
|
||||
};
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
/// Write the generated table to stdout (rather than to `crates/ty/docs/configuration.md`).
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
|
||||
let mut output = String::new();
|
||||
let file_name = "crates/ty/docs/configuration.md";
|
||||
let markdown_path = PathBuf::from(ROOT_DIR).join(file_name);
|
||||
|
||||
generate_set(
|
||||
&mut output,
|
||||
Set::Toplevel(Options::metadata()),
|
||||
&mut Vec::new(),
|
||||
);
|
||||
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{output}");
|
||||
}
|
||||
Mode::Check => {
|
||||
let current = std::fs::read_to_string(&markdown_path)?;
|
||||
if output == current {
|
||||
println!("Up-to-date: {file_name}",);
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &output);
|
||||
bail!("{file_name} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}",);
|
||||
}
|
||||
}
|
||||
Mode::Write => {
|
||||
let current = std::fs::read_to_string(&markdown_path)?;
|
||||
if current == output {
|
||||
println!("Up-to-date: {file_name}",);
|
||||
} else {
|
||||
println!("Updating: {file_name}",);
|
||||
std::fs::write(markdown_path, output.as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
|
||||
match &set {
|
||||
Set::Toplevel(_) => {
|
||||
output.push_str("# Configuration\n");
|
||||
}
|
||||
Set::Named { name, .. } => {
|
||||
let title = parents
|
||||
.iter()
|
||||
.filter_map(|set| set.name())
|
||||
.chain(std::iter::once(name.as_str()))
|
||||
.join(".");
|
||||
writeln!(output, "## `{title}`\n",).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(documentation) = set.metadata().documentation() {
|
||||
output.push_str(documentation);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
let mut visitor = CollectOptionsVisitor::default();
|
||||
set.metadata().record(&mut visitor);
|
||||
|
||||
let (mut fields, mut sets) = (visitor.fields, visitor.groups);
|
||||
|
||||
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
|
||||
parents.push(set);
|
||||
|
||||
// Generate the fields.
|
||||
for (name, field) in &fields {
|
||||
emit_field(output, name, field, parents.as_slice());
|
||||
output.push_str("---\n\n");
|
||||
}
|
||||
|
||||
// Generate all the sub-sets.
|
||||
for (set_name, sub_set) in &sets {
|
||||
generate_set(
|
||||
output,
|
||||
Set::Named {
|
||||
name: set_name.to_string(),
|
||||
set: *sub_set,
|
||||
},
|
||||
parents,
|
||||
);
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
}
|
||||
|
||||
enum Set {
|
||||
Toplevel(OptionSet),
|
||||
Named { name: String, set: OptionSet },
|
||||
}
|
||||
|
||||
impl Set {
|
||||
fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Set::Toplevel(_) => None,
|
||||
Set::Named { name, .. } => Some(name),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &OptionSet {
|
||||
match self {
|
||||
Set::Toplevel(set) => set,
|
||||
Set::Named { set, .. } => set,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) {
|
||||
let header_level = if parents.is_empty() { "###" } else { "####" };
|
||||
|
||||
let _ = writeln!(output, "{header_level} `{name}`");
|
||||
|
||||
output.push('\n');
|
||||
|
||||
if let Some(deprecated) = &field.deprecated {
|
||||
output.push_str("> [!WARN] \"Deprecated\"\n");
|
||||
output.push_str("> This option has been deprecated");
|
||||
|
||||
if let Some(since) = deprecated.since {
|
||||
write!(output, " in {since}").unwrap();
|
||||
}
|
||||
|
||||
output.push('.');
|
||||
|
||||
if let Some(message) = deprecated.message {
|
||||
writeln!(output, " {message}").unwrap();
|
||||
}
|
||||
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output.push_str(field.doc);
|
||||
output.push_str("\n\n");
|
||||
let _ = writeln!(output, "**Default value**: `{}`", field.default);
|
||||
output.push('\n');
|
||||
let _ = writeln!(output, "**Type**: `{}`", field.value_type);
|
||||
output.push('\n');
|
||||
output.push_str("**Example usage** (`pyproject.toml`):\n\n");
|
||||
output.push_str(&format_example(
|
||||
&format_header(
|
||||
field.scope,
|
||||
field.example,
|
||||
parents,
|
||||
ConfigurationFile::PyprojectToml,
|
||||
),
|
||||
field.example,
|
||||
));
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
fn format_example(header: &str, content: &str) -> String {
|
||||
if header.is_empty() {
|
||||
format!("```toml\n{content}\n```\n",)
|
||||
} else {
|
||||
format!("```toml\n{header}\n{content}\n```\n",)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the TOML header for the example usage for a given option.
|
||||
///
|
||||
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
|
||||
fn format_header(
|
||||
scope: Option<&str>,
|
||||
example: &str,
|
||||
parents: &[Set],
|
||||
configuration: ConfigurationFile,
|
||||
) -> String {
|
||||
let tool_parent = match configuration {
|
||||
ConfigurationFile::PyprojectToml => Some("tool.ty"),
|
||||
ConfigurationFile::TyToml => None,
|
||||
};
|
||||
|
||||
let header = tool_parent
|
||||
.into_iter()
|
||||
.chain(parents.iter().filter_map(|parent| parent.name()))
|
||||
.chain(scope)
|
||||
.join(".");
|
||||
|
||||
// Ex) `[[tool.ty.xx]]`
|
||||
if example.starts_with(&format!("[[{header}")) {
|
||||
return String::new();
|
||||
}
|
||||
// Ex) `[tool.ty.rules]`
|
||||
if example.starts_with(&format!("[{header}")) {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
if header.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("[{header}]")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CollectOptionsVisitor {
|
||||
groups: Vec<(String, OptionSet)>,
|
||||
fields: Vec<(String, OptionField)>,
|
||||
}
|
||||
|
||||
impl Visit for CollectOptionsVisitor {
|
||||
fn record_set(&mut self, name: &str, group: OptionSet) {
|
||||
self.groups.push((name.to_owned(), group));
|
||||
}
|
||||
|
||||
fn record_field(&mut self, name: &str, field: OptionField) {
|
||||
self.fields.push((name.to_owned(), field));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum ConfigurationFile {
|
||||
PyprojectToml,
|
||||
#[expect(dead_code)]
|
||||
TyToml,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{main, Args};
|
||||
|
||||
#[test]
|
||||
fn ty_configuration_markdown_up_to_date() -> Result<()> {
|
||||
main(&Args { mode: Mode::Check })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
143
crates/ruff_dev/src/generate_ty_rules.rs
Normal file
143
crates/ruff_dev/src/generate_ty_rules.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
//! Generates the rules table for ty
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use itertools::Itertools as _;
|
||||
use pretty_assertions::StrComparison;
|
||||
|
||||
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
/// Write the generated table to stdout (rather than to `ty.schema.json`).
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
let markdown = generate_markdown();
|
||||
let filename = "crates/ty/docs/rules.md";
|
||||
let schema_path = PathBuf::from(ROOT_DIR).join(filename);
|
||||
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{markdown}");
|
||||
}
|
||||
Mode::Check => {
|
||||
let current = fs::read_to_string(schema_path)?;
|
||||
if current == markdown {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &markdown);
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
|
||||
}
|
||||
}
|
||||
Mode::Write => {
|
||||
let current = fs::read_to_string(&schema_path)?;
|
||||
if current == markdown {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
println!("Updating: {filename}");
|
||||
fs::write(schema_path, markdown.as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_markdown() -> String {
|
||||
let registry = &*ty_project::DEFAULT_LINT_REGISTRY;
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
let _ = writeln!(&mut output, "# Rules\n");
|
||||
|
||||
let mut lints: Vec<_> = registry.lints().iter().collect();
|
||||
lints.sort_by(|a, b| {
|
||||
a.default_level()
|
||||
.cmp(&b.default_level())
|
||||
.reverse()
|
||||
.then_with(|| a.name().cmp(&b.name()))
|
||||
});
|
||||
|
||||
for lint in lints {
|
||||
let _ = writeln!(&mut output, "## `{rule_name}`\n", rule_name = lint.name());
|
||||
|
||||
// Increase the header-level by one
|
||||
let documentation = lint
|
||||
.documentation_lines()
|
||||
.map(|line| {
|
||||
if line.starts_with('#') {
|
||||
Cow::Owned(format!("#{line}"))
|
||||
} else {
|
||||
Cow::Borrowed(line)
|
||||
}
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
let _ = writeln!(
|
||||
&mut output,
|
||||
r#"**Default level**: {level}
|
||||
|
||||
<details>
|
||||
<summary>{summary}</summary>
|
||||
|
||||
{documentation}
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name})
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/{file}#L{line})
|
||||
</details>
|
||||
"#,
|
||||
level = lint.default_level(),
|
||||
// GitHub doesn't support markdown in `summary` headers
|
||||
summary = replace_inline_code(lint.summary()),
|
||||
encoded_name = url::form_urlencoded::byte_serialize(lint.name().as_str().as_bytes())
|
||||
.collect::<String>(),
|
||||
file = url::form_urlencoded::byte_serialize(lint.file().replace('\\', "/").as_bytes())
|
||||
.collect::<String>(),
|
||||
line = lint.line(),
|
||||
);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Replaces inline code blocks (`code`) with `<code>code</code>`
|
||||
fn replace_inline_code(input: &str) -> String {
|
||||
let mut output = String::new();
|
||||
let mut parts = input.split('`');
|
||||
|
||||
while let Some(before) = parts.next() {
|
||||
if let Some(between) = parts.next() {
|
||||
output.push_str(before);
|
||||
output.push_str("<code>");
|
||||
output.push_str(between);
|
||||
output.push_str("</code>");
|
||||
} else {
|
||||
output.push_str(before);
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{main, Args};
|
||||
|
||||
#[test]
|
||||
fn ty_rules_up_to_date() -> Result<()> {
|
||||
main(&Args { mode: Mode::Check })
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! Within the ruff repository you can run it with `cargo dev`.
|
||||
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use ruff::{args::GlobalConfigArgs, check};
|
||||
@@ -15,6 +17,9 @@ mod generate_docs;
|
||||
mod generate_json_schema;
|
||||
mod generate_options;
|
||||
mod generate_rules_table;
|
||||
mod generate_ty_cli_reference;
|
||||
mod generate_ty_options;
|
||||
mod generate_ty_rules;
|
||||
mod generate_ty_schema;
|
||||
mod print_ast;
|
||||
mod print_cst;
|
||||
@@ -44,8 +49,10 @@ enum Command {
|
||||
GenerateTySchema(generate_ty_schema::Args),
|
||||
/// Generate a Markdown-compatible table of supported lint rules.
|
||||
GenerateRulesTable,
|
||||
GenerateTyRules(generate_ty_rules::Args),
|
||||
/// Generate a Markdown-compatible listing of configuration options.
|
||||
GenerateOptions,
|
||||
GenerateTyOptions(generate_ty_options::Args),
|
||||
/// Generate CLI help.
|
||||
GenerateCliHelp(generate_cli_help::Args),
|
||||
/// Generate Markdown docs.
|
||||
@@ -88,7 +95,9 @@ fn main() -> Result<ExitCode> {
|
||||
Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
|
||||
Command::GenerateTySchema(args) => generate_ty_schema::main(&args)?,
|
||||
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
|
||||
Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?,
|
||||
Command::GenerateOptions => println!("{}", generate_options::generate()),
|
||||
Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?,
|
||||
Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,
|
||||
Command::GenerateDocs(args) => generate_docs::main(&args)?,
|
||||
Command::PrintAST(args) => print_ast::main(&args)?,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Print the AST for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Print the `LibCST` CST for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Print the token stream for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Run round-trip source code generation on a given Python or Jupyter notebook file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -92,7 +92,7 @@ impl std::fmt::Display for IndentStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// The visual width of a indentation.
|
||||
/// The visual width of an indentation.
|
||||
///
|
||||
/// Determines the visual width of a tab character (`\t`) and the number of
|
||||
/// spaces per indent when using [`IndentStyle::Space`].
|
||||
@@ -207,7 +207,7 @@ pub trait FormatOptions {
|
||||
/// What's the max width of a line. Defaults to 80.
|
||||
fn line_width(&self) -> LineWidth;
|
||||
|
||||
/// Derives the print options from the these format options
|
||||
/// Derives the print options from these format options
|
||||
fn as_print_options(&self) -> PrinterOptions;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,8 +88,8 @@ impl Db for ModuleDb {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
@@ -98,6 +98,4 @@ impl Db for ModuleDb {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for ModuleDb {
|
||||
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
|
||||
}
|
||||
impl salsa::Database for ModuleDb {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -15,6 +15,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ruff_annotate_snippets = { workspace = true }
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_diagnostics = { workspace = true, features = ["serde"] }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
from airflow.sdk import Asset
|
||||
from airflow.assets.manager import AssetManager
|
||||
except ModuleNotFoundError:
|
||||
from airflow.datasets import Dataset as Asset
|
||||
from airflow.datasets.manager import DatasetManager as AssetManager
|
||||
|
||||
Asset
|
||||
|
||||
try:
|
||||
from airflow.sdk import Asset
|
||||
except ModuleNotFoundError:
|
||||
from airflow import datasets
|
||||
|
||||
Asset = datasets.Dataset
|
||||
|
||||
asset = Asset()
|
||||
AssetManager()
|
||||
|
||||
@@ -91,14 +91,14 @@ from airflow.operators.sql import (
|
||||
BaseSQLOperator,
|
||||
BranchSQLOperator,
|
||||
SQLColumnCheckOperator,
|
||||
SQLTablecheckOperator,
|
||||
SQLTableCheckOperator,
|
||||
_convert_to_float_if_possible,
|
||||
parse_boolean,
|
||||
)
|
||||
|
||||
BaseSQLOperator()
|
||||
BranchSQLOperator()
|
||||
SQLTablecheckOperator()
|
||||
SQLTableCheckOperator()
|
||||
SQLColumnCheckOperator()
|
||||
_convert_to_float_if_possible()
|
||||
parse_boolean()
|
||||
|
||||
8
crates/ruff_linter/resources/test/fixtures/airflow/AIR302_try.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/airflow/AIR302_try.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
from airflow.providers.http.operators.http import HttpOperator
|
||||
except ModuleNotFoundError:
|
||||
from airflow.operators.http_operator import SimpleHttpOperator as HttpOperator
|
||||
|
||||
HttpOperator()
|
||||
@@ -13,6 +13,10 @@ from airflow.decorators import dag, setup, task, task_group, teardown
|
||||
from airflow.io.path import ObjectStoragePath
|
||||
from airflow.io.storage import attach
|
||||
from airflow.models import DAG as DAGFromModel
|
||||
from airflow.models import (
|
||||
Connection,
|
||||
Variable,
|
||||
)
|
||||
from airflow.models.baseoperator import chain, chain_linear, cross_downstream
|
||||
from airflow.models.baseoperatorlink import BaseOperatorLink
|
||||
from airflow.models.dag import DAG as DAGFromDag
|
||||
@@ -42,7 +46,9 @@ ObjectStoragePath()
|
||||
attach()
|
||||
|
||||
# airflow.models
|
||||
Connection()
|
||||
DAGFromModel()
|
||||
Variable()
|
||||
|
||||
# airflow.models.baseoperator
|
||||
chain()
|
||||
|
||||
8
crates/ruff_linter/resources/test/fixtures/airflow/AIR311_try.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/airflow/AIR311_try.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
from airflow.sdk import Asset
|
||||
except ModuleNotFoundError:
|
||||
from airflow.datasets import Dataset as Asset
|
||||
|
||||
Asset()
|
||||
8
crates/ruff_linter/resources/test/fixtures/airflow/AIR312_try.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/airflow/AIR312_try.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
from airflow.providers.standard.triggers.file import FileTrigger
|
||||
except ModuleNotFoundError:
|
||||
from airflow.triggers.file import FileTrigger
|
||||
|
||||
FileTrigger()
|
||||
@@ -166,3 +166,6 @@ query60 = f"""
|
||||
foo
|
||||
FROM ({user_input}) raw
|
||||
"""
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17967
|
||||
query61 = f"SELECT * FROM table" # skip expressionless f-strings
|
||||
|
||||
@@ -25,3 +25,11 @@ warnings.warn(
|
||||
# some comments here
|
||||
source = None # no trailing comma
|
||||
)
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/18011
|
||||
warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),))
|
||||
# trigger diagnostic if `skip_file_prefixes` is present and set to the default value
|
||||
warnings.warn("test", skip_file_prefixes=())
|
||||
|
||||
_my_prefixes = ("this","that")
|
||||
warnings.warn("test", skip_file_prefixes = _my_prefixes)
|
||||
|
||||
@@ -28,3 +28,14 @@ abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
# Some values need to be parenthesized.
|
||||
abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/18036
|
||||
# The autofix for this is unsafe due to the comments inside the dictionary.
|
||||
foo(
|
||||
**{
|
||||
# Comment 1
|
||||
"x": 1.0,
|
||||
# Comment 2
|
||||
"y": 2.0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ no_sep = None
|
||||
|
||||
" a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
||||
"".split() # []
|
||||
"""
|
||||
"""
|
||||
""".split() # []
|
||||
" ".split() # []
|
||||
"/abc/".split() # ["/abc/"]
|
||||
@@ -73,7 +73,7 @@ r"\n " "\n".split() # [r"\n"]
|
||||
|
||||
# negatives
|
||||
|
||||
# invalid values should not cause panic
|
||||
# invalid values should not cause panic
|
||||
"a,b,c,d".split(maxsplit="hello")
|
||||
"a,b,c,d".split(maxsplit=-"hello")
|
||||
|
||||
@@ -106,3 +106,7 @@ b"TesT.WwW.ExamplE.CoM".split(b".")
|
||||
'''itemC'''
|
||||
"'itemD'"
|
||||
""".split()
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/18042
|
||||
print("a,b".rsplit(","))
|
||||
print("a,b,c".rsplit(",", 1))
|
||||
|
||||
@@ -9,3 +9,7 @@ extensions_dir = "./extensions"
|
||||
glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"))
|
||||
list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp")))
|
||||
search("*.png")
|
||||
|
||||
# if `dir_fd` is set, suppress the diagnostic
|
||||
glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1)
|
||||
list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1))
|
||||
|
||||
@@ -87,3 +87,20 @@ def bar(x: int):
|
||||
os.rename("src", "dst", src_dir_fd=3, dst_dir_fd=4)
|
||||
os.rename("src", "dst", src_dir_fd=3)
|
||||
os.rename("src", "dst", dst_dir_fd=4)
|
||||
|
||||
# if `dir_fd` is set, suppress the diagnostic
|
||||
os.readlink(p, dir_fd=1)
|
||||
os.stat(p, dir_fd=2)
|
||||
os.unlink(p, dir_fd=3)
|
||||
os.remove(p, dir_fd=4)
|
||||
os.rmdir(p, dir_fd=5)
|
||||
os.mkdir(p, dir_fd=6)
|
||||
os.chmod(p, dir_fd=7)
|
||||
# `chmod` can also receive a file descriptor in the first argument
|
||||
os.chmod(8)
|
||||
os.chmod(x)
|
||||
|
||||
# if `src_dir_fd` or `dst_dir_fd` are set, suppress the diagnostic
|
||||
os.replace("src", "dst", src_dir_fd=1, dst_dir_fd=2)
|
||||
os.replace("src", "dst", src_dir_fd=1)
|
||||
os.replace("src", "dst", dst_dir_fd=2)
|
||||
|
||||
@@ -144,14 +144,14 @@ def f():
|
||||
|
||||
def f():
|
||||
# make sure that `tmp` is not deleted
|
||||
tmp = 1; result = [] # commment should be protected
|
||||
tmp = 1; result = [] # comment should be protected
|
||||
for i in range(10):
|
||||
result.append(i + 1) # PERF401
|
||||
|
||||
|
||||
def f():
|
||||
# make sure that `tmp` is not deleted
|
||||
result = []; tmp = 1 # commment should be protected
|
||||
result = []; tmp = 1 # comment should be protected
|
||||
for i in range(10):
|
||||
result.append(i + 1) # PERF401
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
num = 1337
|
||||
|
||||
def return_num() -> int:
|
||||
@@ -10,6 +13,7 @@ print(bin(num)[2:]) # FURB116
|
||||
print(oct(1337)[2:]) # FURB116
|
||||
print(hex(1337)[2:]) # FURB116
|
||||
print(bin(1337)[2:]) # FURB116
|
||||
print(bin(+1337)[2:]) # FURB116
|
||||
|
||||
print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
@@ -22,3 +26,19 @@ print(hex(0x1337)[3:])
|
||||
# float and complex numbers should be ignored
|
||||
print(bin(1.0)[2:])
|
||||
print(bin(3.14j)[2:])
|
||||
|
||||
d = datetime.datetime.now(tz=datetime.UTC)
|
||||
# autofix is display-only
|
||||
print(bin(d)[2:])
|
||||
# no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
print(bin(len("xyz").numerator)[2:])
|
||||
|
||||
# autofix is display-only
|
||||
print(bin({0: 1}[0].numerator)[2:])
|
||||
# no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
print(bin(ord("\\").numerator)[2:])
|
||||
print(hex(sys
|
||||
.maxunicode)[2:])
|
||||
|
||||
# for negatives numbers autofix is display-only
|
||||
print(bin(-1)[2:])
|
||||
|
||||
@@ -19,6 +19,9 @@ b'c' in b""
|
||||
b"a" in bytearray()
|
||||
b"a" in bytes()
|
||||
1 in frozenset()
|
||||
1 in set(set())
|
||||
2 in frozenset([])
|
||||
'' in set("")
|
||||
|
||||
# OK
|
||||
1 in [2]
|
||||
@@ -35,3 +38,7 @@ b'c' in b"x"
|
||||
b"a" in bytearray([2])
|
||||
b"a" in bytes("a", "utf-8")
|
||||
1 in frozenset("c")
|
||||
1 in set(set((1,2)))
|
||||
1 in set(set([1]))
|
||||
'' in {""}
|
||||
frozenset() in {frozenset()}
|
||||
|
||||
@@ -1116,6 +1116,95 @@ mod tests {
|
||||
PythonVersion::PY310,
|
||||
"InvalidStarExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"irrefutable_case_pattern_wildcard",
|
||||
"
|
||||
match value:
|
||||
case _:
|
||||
pass
|
||||
case 1:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"IrrefutableCasePattern"
|
||||
)]
|
||||
#[test_case(
|
||||
"irrefutable_case_pattern_capture",
|
||||
"
|
||||
match value:
|
||||
case irrefutable:
|
||||
pass
|
||||
case 1:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"IrrefutableCasePattern"
|
||||
)]
|
||||
#[test_case(
|
||||
"single_starred_assignment",
|
||||
"*a = [1, 2, 3, 4]",
|
||||
PythonVersion::PY310,
|
||||
"SingleStarredAssignment"
|
||||
)]
|
||||
#[test_case(
|
||||
"write_to_debug",
|
||||
"
|
||||
__debug__ = False
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"WriteToDebug"
|
||||
)]
|
||||
#[test_case(
|
||||
"write_to_debug_in_function_param",
|
||||
"
|
||||
def process(__debug__):
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"WriteToDebug"
|
||||
)]
|
||||
#[test_case(
|
||||
"write_to_debug_class_type_param",
|
||||
"
|
||||
class Generic[__debug__]:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"WriteToDebug"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_expression_yield_in_type_param",
|
||||
"
|
||||
type X[T: (yield 1)] = int
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"InvalidExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_expression_yield_in_type_alias",
|
||||
"
|
||||
type Y = (yield 1)
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"InvalidExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_expression_walrus_in_return_annotation",
|
||||
"
|
||||
def f[T](x: int) -> (y := 3): return x
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"InvalidExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_expression_yield_from_in_base_class",
|
||||
"
|
||||
class C[T]((yield from [object])):
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"InvalidExpression"
|
||||
)]
|
||||
fn test_semantic_errors(
|
||||
name: &str,
|
||||
contents: &str,
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Emitter for AzureEmitter {
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let location = if context.is_notebook(message.filename()) {
|
||||
let location = if context.is_notebook(&message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
LineColumn::default()
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::text_helpers::ShowNonprinting;
|
||||
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
|
||||
pub(super) struct Diff<'a> {
|
||||
fix: &'a Fix,
|
||||
source_code: &'a SourceFile,
|
||||
source_code: SourceFile,
|
||||
}
|
||||
|
||||
impl<'a> Diff<'a> {
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Emitter for GithubEmitter {
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let source_location = message.compute_start_location();
|
||||
let location = if context.is_notebook(message.filename()) {
|
||||
let location = if context.is_notebook(&message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
LineColumn::default()
|
||||
@@ -43,7 +43,7 @@ impl Emitter for GithubEmitter {
|
||||
write!(
|
||||
writer,
|
||||
"{path}:{row}:{column}:",
|
||||
path = relativize_path(message.filename()),
|
||||
path = relativize_path(&*message.filename()),
|
||||
row = location.line,
|
||||
column = location.column,
|
||||
)?;
|
||||
|
||||
@@ -62,7 +62,7 @@ impl Serialize for SerializedMessages<'_> {
|
||||
let start_location = message.compute_start_location();
|
||||
let end_location = message.compute_end_location();
|
||||
|
||||
let lines = if self.context.is_notebook(message.filename()) {
|
||||
let lines = if self.context.is_notebook(&message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
json!({
|
||||
@@ -77,8 +77,8 @@ impl Serialize for SerializedMessages<'_> {
|
||||
};
|
||||
|
||||
let path = self.project_dir.as_ref().map_or_else(
|
||||
|| relativize_path(message.filename()),
|
||||
|project_dir| relativize_path_to(message.filename(), project_dir),
|
||||
|| relativize_path(&*message.filename()),
|
||||
|project_dir| relativize_path_to(&*message.filename(), project_dir),
|
||||
);
|
||||
|
||||
let mut message_fingerprint = fingerprint(message, &path, 0);
|
||||
|
||||
@@ -65,7 +65,7 @@ impl Emitter for GroupedEmitter {
|
||||
let column_length = calculate_print_width(max_column_length);
|
||||
|
||||
// Print the filename.
|
||||
writeln!(writer, "{}:", relativize_path(filename).underline())?;
|
||||
writeln!(writer, "{}:", relativize_path(&*filename).underline())?;
|
||||
|
||||
// Print each message.
|
||||
for message in messages {
|
||||
@@ -73,7 +73,7 @@ impl Emitter for GroupedEmitter {
|
||||
writer,
|
||||
"{}",
|
||||
DisplayGroupedMessage {
|
||||
notebook_index: context.notebook_index(message.filename()),
|
||||
notebook_index: context.notebook_index(&message.filename()),
|
||||
message,
|
||||
show_fix_status: self.show_fix_status,
|
||||
unsafe_fixes: self.unsafe_fixes,
|
||||
|
||||
@@ -49,8 +49,9 @@ impl Serialize for ExpandedMessages<'_> {
|
||||
}
|
||||
|
||||
pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value {
|
||||
let source_code = message.source_file().to_source_code();
|
||||
let notebook_index = context.notebook_index(message.filename());
|
||||
let source_file = message.source_file();
|
||||
let source_code = source_file.to_source_code();
|
||||
let notebook_index = context.notebook_index(&message.filename());
|
||||
|
||||
let fix = message.fix().map(|fix| {
|
||||
json!({
|
||||
|
||||
@@ -32,7 +32,7 @@ impl Emitter for JunitEmitter {
|
||||
report.add_test_suite(test_suite);
|
||||
} else {
|
||||
for (filename, messages) in group_messages_by_filename(messages) {
|
||||
let mut test_suite = TestSuite::new(filename);
|
||||
let mut test_suite = TestSuite::new(&filename);
|
||||
test_suite
|
||||
.extra
|
||||
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
|
||||
@@ -44,7 +44,7 @@ impl Emitter for JunitEmitter {
|
||||
} = message;
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(message.body());
|
||||
let location = if context.is_notebook(message.filename()) {
|
||||
let location = if context.is_notebook(&message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
LineColumn::default()
|
||||
@@ -66,7 +66,7 @@ impl Emitter for JunitEmitter {
|
||||
},
|
||||
status,
|
||||
);
|
||||
let file_path = Path::new(filename);
|
||||
let file_path = Path::new(&*filename);
|
||||
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
|
||||
let classname = file_path.parent().unwrap().join(file_stem);
|
||||
case.set_classname(classname.to_str().unwrap());
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::diagnostic::{self as db, Annotation, DiagnosticId, Severity, Span};
|
||||
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -45,7 +47,7 @@ mod text;
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Message {
|
||||
Diagnostic(DiagnosticMessage),
|
||||
SyntaxError(SyntaxErrorMessage),
|
||||
SyntaxError(db::Diagnostic),
|
||||
}
|
||||
|
||||
/// A diagnostic message corresponding to a rule violation.
|
||||
@@ -59,14 +61,6 @@ pub struct DiagnosticMessage {
|
||||
pub noqa_offset: TextSize,
|
||||
}
|
||||
|
||||
/// A syntax error message raised by the parser.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SyntaxErrorMessage {
|
||||
pub message: String,
|
||||
pub range: TextRange,
|
||||
pub file: SourceFile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum MessageKind {
|
||||
Diagnostic(Rule),
|
||||
@@ -83,6 +77,17 @@ impl MessageKind {
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn syntax_error(
|
||||
message: impl std::fmt::Display,
|
||||
range: TextRange,
|
||||
file: SourceFile,
|
||||
) -> Message {
|
||||
let mut diag = db::Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
|
||||
let span = Span::from(file).with_range(range);
|
||||
diag.annotate(Annotation::primary(span).message(message));
|
||||
Self::SyntaxError(diag)
|
||||
}
|
||||
|
||||
/// Create a [`Message`] from the given [`Diagnostic`] corresponding to a rule violation.
|
||||
pub fn from_diagnostic(
|
||||
diagnostic: Diagnostic,
|
||||
@@ -114,14 +119,14 @@ impl Message {
|
||||
.next()
|
||||
.map_or(TextSize::new(0), TextLen::text_len);
|
||||
|
||||
Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: format!(
|
||||
Message::syntax_error(
|
||||
format_args!(
|
||||
"SyntaxError: {}",
|
||||
DisplayParseErrorType::new(&parse_error.error)
|
||||
),
|
||||
range: TextRange::at(parse_error.location.start(), len),
|
||||
TextRange::at(parse_error.location.start(), len),
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a [`Message`] from the given [`UnsupportedSyntaxError`].
|
||||
@@ -129,11 +134,11 @@ impl Message {
|
||||
unsupported_syntax_error: &UnsupportedSyntaxError,
|
||||
file: SourceFile,
|
||||
) -> Message {
|
||||
Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: format!("SyntaxError: {unsupported_syntax_error}"),
|
||||
range: unsupported_syntax_error.range,
|
||||
Message::syntax_error(
|
||||
format_args!("SyntaxError: {unsupported_syntax_error}"),
|
||||
unsupported_syntax_error.range,
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a [`Message`] from the given [`SemanticSyntaxError`].
|
||||
@@ -141,11 +146,11 @@ impl Message {
|
||||
semantic_syntax_error: &SemanticSyntaxError,
|
||||
file: SourceFile,
|
||||
) -> Message {
|
||||
Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: format!("SyntaxError: {semantic_syntax_error}"),
|
||||
range: semantic_syntax_error.range,
|
||||
Message::syntax_error(
|
||||
format_args!("SyntaxError: {semantic_syntax_error}"),
|
||||
semantic_syntax_error.range,
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> {
|
||||
@@ -168,8 +173,11 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is a syntax error message.
|
||||
pub const fn is_syntax_error(&self) -> bool {
|
||||
matches!(self, Message::SyntaxError(_))
|
||||
pub fn is_syntax_error(&self) -> bool {
|
||||
match self {
|
||||
Message::Diagnostic(_) => false,
|
||||
Message::SyntaxError(diag) => diag.id().is_invalid_syntax(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a message kind.
|
||||
@@ -192,7 +200,11 @@ impl Message {
|
||||
pub fn body(&self) -> &str {
|
||||
match self {
|
||||
Message::Diagnostic(m) => &m.kind.body,
|
||||
Message::SyntaxError(m) => &m.message,
|
||||
Message::SyntaxError(m) => m
|
||||
.primary_annotation()
|
||||
.expect("Expected a primary annotation for a ruff diagnostic")
|
||||
.get_message()
|
||||
.expect("Expected a message for a ruff diagnostic"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,27 +246,47 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Returns the filename for the message.
|
||||
pub fn filename(&self) -> &str {
|
||||
self.source_file().name()
|
||||
pub fn filename(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
Message::Diagnostic(m) => Cow::Borrowed(m.file.name()),
|
||||
Message::SyntaxError(diag) => Cow::Owned(
|
||||
diag.expect_primary_span()
|
||||
.expect_ruff_file()
|
||||
.name()
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the start source location for the message.
|
||||
pub fn compute_start_location(&self) -> LineColumn {
|
||||
self.source_file()
|
||||
.to_source_code()
|
||||
.line_column(self.start())
|
||||
match self {
|
||||
Message::Diagnostic(m) => m.file.to_source_code().line_column(m.range.start()),
|
||||
Message::SyntaxError(diag) => diag
|
||||
.expect_primary_span()
|
||||
.expect_ruff_file()
|
||||
.to_source_code()
|
||||
.line_column(self.start()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the end source location for the message.
|
||||
pub fn compute_end_location(&self) -> LineColumn {
|
||||
self.source_file().to_source_code().line_column(self.end())
|
||||
match self {
|
||||
Message::Diagnostic(m) => m.file.to_source_code().line_column(m.range.end()),
|
||||
Message::SyntaxError(diag) => diag
|
||||
.expect_primary_span()
|
||||
.expect_ruff_file()
|
||||
.to_source_code()
|
||||
.line_column(self.end()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`SourceFile`] which the message belongs to.
|
||||
pub fn source_file(&self) -> &SourceFile {
|
||||
pub fn source_file(&self) -> SourceFile {
|
||||
match self {
|
||||
Message::Diagnostic(m) => &m.file,
|
||||
Message::SyntaxError(m) => &m.file,
|
||||
Message::Diagnostic(m) => m.file.clone(),
|
||||
Message::SyntaxError(m) => m.expect_primary_span().expect_ruff_file().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,7 +307,10 @@ impl Ranged for Message {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Message::Diagnostic(m) => m.range,
|
||||
Message::SyntaxError(m) => m.range,
|
||||
Message::SyntaxError(m) => m
|
||||
.expect_primary_span()
|
||||
.range()
|
||||
.expect("Expected range for ruff span"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,11 +328,11 @@ impl Deref for MessageWithLocation<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<MessageWithLocation>> {
|
||||
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<String, Vec<MessageWithLocation>> {
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for message in messages {
|
||||
grouped_messages
|
||||
.entry(message.filename())
|
||||
.entry(message.filename().to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(MessageWithLocation {
|
||||
message,
|
||||
|
||||
@@ -18,7 +18,7 @@ impl Emitter for PylintEmitter {
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let row = if context.is_notebook(message.filename()) {
|
||||
let row = if context.is_notebook(&message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
OneIndexed::from_zero_indexed(0)
|
||||
@@ -39,7 +39,7 @@ impl Emitter for PylintEmitter {
|
||||
writeln!(
|
||||
writer,
|
||||
"{path}:{row}: {body}",
|
||||
path = relativize_path(message.filename()),
|
||||
path = relativize_path(&*message.filename()),
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ impl Serialize for ExpandedMessages<'_> {
|
||||
}
|
||||
|
||||
fn message_to_rdjson_value(message: &Message) -> Value {
|
||||
let source_code = message.source_file().to_source_code();
|
||||
let source_file = message.source_file();
|
||||
let source_code = source_file.to_source_code();
|
||||
|
||||
let start_location = source_code.line_column(message.start());
|
||||
let end_location = source_code.line_column(message.end());
|
||||
|
||||
@@ -121,7 +121,7 @@ impl SarifResult {
|
||||
fn from_message(message: &Message) -> Result<Self> {
|
||||
let start_location = message.compute_start_location();
|
||||
let end_location = message.compute_end_location();
|
||||
let path = normalize_path(message.filename());
|
||||
let path = normalize_path(&*message.filename());
|
||||
Ok(Self {
|
||||
rule: message.rule(),
|
||||
level: "error".to_string(),
|
||||
@@ -141,7 +141,7 @@ impl SarifResult {
|
||||
fn from_message(message: &Message) -> Result<Self> {
|
||||
let start_location = message.compute_start_location();
|
||||
let end_location = message.compute_end_location();
|
||||
let path = normalize_path(message.filename());
|
||||
let path = normalize_path(&*message.filename());
|
||||
Ok(Self {
|
||||
rule: message.rule(),
|
||||
level: "error".to_string(),
|
||||
|
||||
@@ -73,12 +73,12 @@ impl Emitter for TextEmitter {
|
||||
write!(
|
||||
writer,
|
||||
"{path}{sep}",
|
||||
path = relativize_path(message.filename()).bold(),
|
||||
path = relativize_path(&*message.filename()).bold(),
|
||||
sep = ":".cyan(),
|
||||
)?;
|
||||
|
||||
let start_location = message.compute_start_location();
|
||||
let notebook_index = context.notebook_index(message.filename());
|
||||
let notebook_index = context.notebook_index(&message.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 {
|
||||
@@ -191,7 +191,8 @@ impl Display for MessageCodeFrame<'_> {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let source_code = self.message.source_file().to_source_code();
|
||||
let source_file = self.message.source_file();
|
||||
let source_code = source_file.to_source_code();
|
||||
|
||||
let content_start_index = source_code.line_index(self.message.start());
|
||||
let mut start_index = content_start_index.saturating_sub(2);
|
||||
|
||||
@@ -45,7 +45,8 @@ pub(crate) enum ProviderReplacement {
|
||||
|
||||
pub(crate) fn is_guarded_by_try_except(
|
||||
expr: &Expr,
|
||||
replacement: &Replacement,
|
||||
module: &str,
|
||||
name: &str,
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
match expr {
|
||||
@@ -63,7 +64,7 @@ pub(crate) fn is_guarded_by_try_except(
|
||||
if !suspended_exceptions.contains(Exceptions::ATTRIBUTE_ERROR) {
|
||||
return false;
|
||||
}
|
||||
try_block_contains_undeprecated_attribute(try_node, replacement, semantic)
|
||||
try_block_contains_undeprecated_attribute(try_node, module, name, semantic)
|
||||
}
|
||||
Expr::Name(ExprName { id, .. }) => {
|
||||
let Some(binding_id) = semantic.lookup_symbol(id.as_str()) else {
|
||||
@@ -89,7 +90,7 @@ pub(crate) fn is_guarded_by_try_except(
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try_block_contains_undeprecated_import(try_node, replacement)
|
||||
try_block_contains_undeprecated_import(try_node, module, name)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
@@ -100,12 +101,10 @@ pub(crate) fn is_guarded_by_try_except(
|
||||
/// member is being accessed from the non-deprecated location?
|
||||
fn try_block_contains_undeprecated_attribute(
|
||||
try_node: &StmtTry,
|
||||
replacement: &Replacement,
|
||||
module: &str,
|
||||
name: &str,
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
let Replacement::AutoImport { module, name } = replacement else {
|
||||
return false;
|
||||
};
|
||||
let undeprecated_qualified_name = {
|
||||
let mut builder = QualifiedNameBuilder::default();
|
||||
for part in module.split('.') {
|
||||
@@ -122,10 +121,7 @@ fn try_block_contains_undeprecated_attribute(
|
||||
/// Given an [`ast::StmtTry`] node, does the `try` branch of that node
|
||||
/// contain any [`ast::StmtImportFrom`] nodes that indicate the airflow
|
||||
/// member is being imported from the non-deprecated location?
|
||||
fn try_block_contains_undeprecated_import(try_node: &StmtTry, replacement: &Replacement) -> bool {
|
||||
let Replacement::AutoImport { module, name } = replacement else {
|
||||
return false;
|
||||
};
|
||||
fn try_block_contains_undeprecated_import(try_node: &StmtTry, module: &str, name: &str) -> bool {
|
||||
let mut import_searcher = ImportSearcher::new(module, name);
|
||||
import_searcher.visit_body(&try_node.body);
|
||||
import_searcher.found_import
|
||||
|
||||
@@ -46,9 +46,12 @@ mod tests {
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_sqlite.py"))]
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_zendesk.py"))]
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_standard.py"))]
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_try.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_args.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_names.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_try.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedToMoveToProvider, Path::new("AIR312.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedToMoveToProvider, Path::new("AIR312_try.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::rules::airflow::helpers::ProviderReplacement;
|
||||
use crate::rules::airflow::helpers::{is_guarded_by_try_except, ProviderReplacement};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{Expr, ExprAttribute};
|
||||
@@ -288,7 +288,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
|
||||
}
|
||||
}
|
||||
["airflow", "operators", "sql", rest] => match *rest {
|
||||
"BaseSQLOperator" | "BranchSQLOperator" | "SQLTablecheckOperator" => {
|
||||
"BaseSQLOperator" | "BranchSQLOperator" | "SQLTableCheckOperator" => {
|
||||
ProviderReplacement::SourceModuleMovedToProvider {
|
||||
name: (*rest).to_string(),
|
||||
module: "airflow.providers.common.sql.operators.sql",
|
||||
@@ -937,13 +937,17 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
|
||||
ranged.range(),
|
||||
);
|
||||
|
||||
if let ProviderReplacement::AutoImport {
|
||||
module,
|
||||
name,
|
||||
provider: _,
|
||||
version: _,
|
||||
} = replacement
|
||||
{
|
||||
let semantic = checker.semantic();
|
||||
if let Some((module, name)) = match &replacement {
|
||||
ProviderReplacement::AutoImport { module, name, .. } => Some((module, *name)),
|
||||
ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => {
|
||||
Some((module, name.as_str()))
|
||||
}
|
||||
_ => None,
|
||||
} {
|
||||
if is_guarded_by_try_except(expr, module, name, semantic) {
|
||||
return;
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(module, name),
|
||||
@@ -954,6 +958,5 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
|
||||
Ok(Fix::safe_edits(import_edit, [replacement_edit]))
|
||||
});
|
||||
}
|
||||
|
||||
checker.report_diagnostic(diagnostic);
|
||||
}
|
||||
|
||||
@@ -865,10 +865,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if is_guarded_by_try_except(expr, &replacement, semantic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
Airflow3Removal {
|
||||
deprecated: qualified_name.to_string(),
|
||||
@@ -876,8 +872,15 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
},
|
||||
range,
|
||||
);
|
||||
|
||||
if let Replacement::AutoImport { module, name } = replacement {
|
||||
let semantic = checker.semantic();
|
||||
if let Some((module, name)) = match &replacement {
|
||||
Replacement::AutoImport { module, name } => Some((module, *name)),
|
||||
Replacement::SourceModuleMoved { module, name } => Some((module, name.as_str())),
|
||||
_ => None,
|
||||
} {
|
||||
if is_guarded_by_try_except(expr, module, name, semantic) {
|
||||
return;
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(module, name),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::importer::ImportRequest;
|
||||
|
||||
use crate::rules::airflow::helpers::ProviderReplacement;
|
||||
use crate::rules::airflow::helpers::{is_guarded_by_try_except, ProviderReplacement};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{Expr, ExprAttribute};
|
||||
@@ -279,13 +279,17 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
|
||||
ranged.range(),
|
||||
);
|
||||
|
||||
if let ProviderReplacement::AutoImport {
|
||||
module,
|
||||
name,
|
||||
provider: _,
|
||||
version: _,
|
||||
} = replacement
|
||||
{
|
||||
let semantic = checker.semantic();
|
||||
if let Some((module, name)) = match &replacement {
|
||||
ProviderReplacement::AutoImport { module, name, .. } => Some((module, *name)),
|
||||
ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => {
|
||||
Some((module, name.as_str()))
|
||||
}
|
||||
_ => None,
|
||||
} {
|
||||
if is_guarded_by_try_except(expr, module, name, semantic) {
|
||||
return;
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(module, name),
|
||||
|
||||
@@ -242,6 +242,14 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
name: "attach".to_string(),
|
||||
},
|
||||
|
||||
// airflow.models
|
||||
["airflow", "models", rest @ ("Connection" | "Variable")] => {
|
||||
Replacement::SourceModuleMoved {
|
||||
module: "airflow.sdk",
|
||||
name: (*rest).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
// airflow.models.baseoperator
|
||||
["airflow", "models", "baseoperator", rest] => match *rest {
|
||||
"chain" | "chain_linear" | "cross_downstream" => Replacement::SourceModuleMoved {
|
||||
@@ -275,10 +283,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if is_guarded_by_try_except(expr, &replacement, semantic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
Airflow3SuggestedUpdate {
|
||||
deprecated: qualified_name.to_string(),
|
||||
@@ -287,7 +291,15 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
range,
|
||||
);
|
||||
|
||||
if let Replacement::AutoImport { module, name } = replacement {
|
||||
let semantic = checker.semantic();
|
||||
if let Some((module, name)) = match &replacement {
|
||||
Replacement::AutoImport { module, name } => Some((module, *name)),
|
||||
Replacement::SourceModuleMoved { module, name } => Some((module, name.as_str())),
|
||||
_ => None,
|
||||
} {
|
||||
if is_guarded_by_try_except(expr, module, name, semantic) {
|
||||
return;
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(module, name),
|
||||
|
||||
@@ -216,7 +216,7 @@ AIR302_common_sql.py:99:1: AIR302 `airflow.operators.sql.BaseSQLOperator` is mov
|
||||
99 | BaseSQLOperator()
|
||||
| ^^^^^^^^^^^^^^^ AIR302
|
||||
100 | BranchSQLOperator()
|
||||
101 | SQLTablecheckOperator()
|
||||
101 | SQLTableCheckOperator()
|
||||
|
|
||||
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BaseSQLOperator` instead.
|
||||
|
||||
@@ -225,26 +225,26 @@ AIR302_common_sql.py:100:1: AIR302 `airflow.operators.sql.BranchSQLOperator` is
|
||||
99 | BaseSQLOperator()
|
||||
100 | BranchSQLOperator()
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
101 | SQLTablecheckOperator()
|
||||
101 | SQLTableCheckOperator()
|
||||
102 | SQLColumnCheckOperator()
|
||||
|
|
||||
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BranchSQLOperator` instead.
|
||||
|
||||
AIR302_common_sql.py:101:1: AIR302 `airflow.operators.sql.SQLTablecheckOperator` is moved into `common-sql` provider in Airflow 3.0;
|
||||
AIR302_common_sql.py:101:1: AIR302 `airflow.operators.sql.SQLTableCheckOperator` is moved into `common-sql` provider in Airflow 3.0;
|
||||
|
|
||||
99 | BaseSQLOperator()
|
||||
100 | BranchSQLOperator()
|
||||
101 | SQLTablecheckOperator()
|
||||
101 | SQLTableCheckOperator()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
102 | SQLColumnCheckOperator()
|
||||
103 | _convert_to_float_if_possible()
|
||||
|
|
||||
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLTablecheckOperator` instead.
|
||||
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLTableCheckOperator` instead.
|
||||
|
||||
AIR302_common_sql.py:102:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator` is moved into `common-sql` provider in Airflow 3.0;
|
||||
|
|
||||
100 | BranchSQLOperator()
|
||||
101 | SQLTablecheckOperator()
|
||||
101 | SQLTableCheckOperator()
|
||||
102 | SQLColumnCheckOperator()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
103 | _convert_to_float_if_possible()
|
||||
@@ -254,7 +254,7 @@ AIR302_common_sql.py:102:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator
|
||||
|
||||
AIR302_common_sql.py:103:1: AIR302 `airflow.operators.sql._convert_to_float_if_possible` is moved into `common-sql` provider in Airflow 3.0;
|
||||
|
|
||||
101 | SQLTablecheckOperator()
|
||||
101 | SQLTableCheckOperator()
|
||||
102 | SQLColumnCheckOperator()
|
||||
103 | _convert_to_float_if_possible()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
|
||||
@@ -1,337 +1,394 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
AIR311_names.py:23:1: AIR311 [*] `airflow.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:27:1: AIR311 [*] `airflow.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
22 | # airflow
|
||||
23 | DatasetFromRoot()
|
||||
26 | # airflow
|
||||
27 | DatasetFromRoot()
|
||||
| ^^^^^^^^^^^^^^^ AIR311
|
||||
24 |
|
||||
25 | # airflow.datasets
|
||||
28 |
|
||||
29 | # airflow.datasets
|
||||
|
|
||||
= help: Use `airflow.sdk.Asset` instead
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | from airflow.models.dag import DAG as DAGFromDag
|
||||
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
21 |+from airflow.sdk import Asset
|
||||
21 22 |
|
||||
22 23 | # airflow
|
||||
23 |-DatasetFromRoot()
|
||||
24 |+Asset()
|
||||
24 25 |
|
||||
25 26 | # airflow.datasets
|
||||
26 27 | Dataset()
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.sdk import Asset
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 |-DatasetFromRoot()
|
||||
28 |+Asset()
|
||||
28 29 |
|
||||
29 30 | # airflow.datasets
|
||||
30 31 | Dataset()
|
||||
|
||||
AIR311_names.py:26:1: AIR311 [*] `airflow.datasets.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:30:1: AIR311 [*] `airflow.datasets.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
25 | # airflow.datasets
|
||||
26 | Dataset()
|
||||
29 | # airflow.datasets
|
||||
30 | Dataset()
|
||||
| ^^^^^^^ AIR311
|
||||
27 | DatasetAlias()
|
||||
28 | DatasetAll()
|
||||
31 | DatasetAlias()
|
||||
32 | DatasetAll()
|
||||
|
|
||||
= help: Use `airflow.sdk.Asset` instead
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | from airflow.models.dag import DAG as DAGFromDag
|
||||
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
21 |+from airflow.sdk import Asset
|
||||
21 22 |
|
||||
22 23 | # airflow
|
||||
23 24 | DatasetFromRoot()
|
||||
24 25 |
|
||||
25 26 | # airflow.datasets
|
||||
26 |-Dataset()
|
||||
27 |+Asset()
|
||||
27 28 | DatasetAlias()
|
||||
28 29 | DatasetAll()
|
||||
29 30 | DatasetAny()
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.sdk import Asset
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 28 | DatasetFromRoot()
|
||||
28 29 |
|
||||
29 30 | # airflow.datasets
|
||||
30 |-Dataset()
|
||||
31 |+Asset()
|
||||
31 32 | DatasetAlias()
|
||||
32 33 | DatasetAll()
|
||||
33 34 | DatasetAny()
|
||||
|
||||
AIR311_names.py:27:1: AIR311 [*] `airflow.datasets.DatasetAlias` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:31:1: AIR311 [*] `airflow.datasets.DatasetAlias` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
25 | # airflow.datasets
|
||||
26 | Dataset()
|
||||
27 | DatasetAlias()
|
||||
29 | # airflow.datasets
|
||||
30 | Dataset()
|
||||
31 | DatasetAlias()
|
||||
| ^^^^^^^^^^^^ AIR311
|
||||
28 | DatasetAll()
|
||||
29 | DatasetAny()
|
||||
32 | DatasetAll()
|
||||
33 | DatasetAny()
|
||||
|
|
||||
= help: Use `airflow.sdk.AssetAlias` instead
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | from airflow.models.dag import DAG as DAGFromDag
|
||||
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
21 |+from airflow.sdk import AssetAlias
|
||||
21 22 |
|
||||
22 23 | # airflow
|
||||
23 24 | DatasetFromRoot()
|
||||
24 25 |
|
||||
25 26 | # airflow.datasets
|
||||
26 27 | Dataset()
|
||||
27 |-DatasetAlias()
|
||||
28 |+AssetAlias()
|
||||
28 29 | DatasetAll()
|
||||
29 30 | DatasetAny()
|
||||
30 31 | Metadata()
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.sdk import AssetAlias
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 28 | DatasetFromRoot()
|
||||
28 29 |
|
||||
29 30 | # airflow.datasets
|
||||
30 31 | Dataset()
|
||||
31 |-DatasetAlias()
|
||||
32 |+AssetAlias()
|
||||
32 33 | DatasetAll()
|
||||
33 34 | DatasetAny()
|
||||
34 35 | Metadata()
|
||||
|
||||
AIR311_names.py:28:1: AIR311 [*] `airflow.datasets.DatasetAll` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:32:1: AIR311 [*] `airflow.datasets.DatasetAll` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
26 | Dataset()
|
||||
27 | DatasetAlias()
|
||||
28 | DatasetAll()
|
||||
30 | Dataset()
|
||||
31 | DatasetAlias()
|
||||
32 | DatasetAll()
|
||||
| ^^^^^^^^^^ AIR311
|
||||
29 | DatasetAny()
|
||||
30 | Metadata()
|
||||
33 | DatasetAny()
|
||||
34 | Metadata()
|
||||
|
|
||||
= help: Use `airflow.sdk.AssetAll` instead
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | from airflow.models.dag import DAG as DAGFromDag
|
||||
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
21 |+from airflow.sdk import AssetAll
|
||||
21 22 |
|
||||
22 23 | # airflow
|
||||
23 24 | DatasetFromRoot()
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.sdk import AssetAll
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 28 | DatasetFromRoot()
|
||||
--------------------------------------------------------------------------------
|
||||
25 26 | # airflow.datasets
|
||||
26 27 | Dataset()
|
||||
27 28 | DatasetAlias()
|
||||
28 |-DatasetAll()
|
||||
29 |+AssetAll()
|
||||
29 30 | DatasetAny()
|
||||
30 31 | Metadata()
|
||||
31 32 | expand_alias_to_datasets()
|
||||
29 30 | # airflow.datasets
|
||||
30 31 | Dataset()
|
||||
31 32 | DatasetAlias()
|
||||
32 |-DatasetAll()
|
||||
33 |+AssetAll()
|
||||
33 34 | DatasetAny()
|
||||
34 35 | Metadata()
|
||||
35 36 | expand_alias_to_datasets()
|
||||
|
||||
AIR311_names.py:29:1: AIR311 [*] `airflow.datasets.DatasetAny` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:33:1: AIR311 [*] `airflow.datasets.DatasetAny` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
27 | DatasetAlias()
|
||||
28 | DatasetAll()
|
||||
29 | DatasetAny()
|
||||
31 | DatasetAlias()
|
||||
32 | DatasetAll()
|
||||
33 | DatasetAny()
|
||||
| ^^^^^^^^^^ AIR311
|
||||
30 | Metadata()
|
||||
31 | expand_alias_to_datasets()
|
||||
34 | Metadata()
|
||||
35 | expand_alias_to_datasets()
|
||||
|
|
||||
= help: Use `airflow.sdk.AssetAny` instead
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | from airflow.models.dag import DAG as DAGFromDag
|
||||
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
21 |+from airflow.sdk import AssetAny
|
||||
21 22 |
|
||||
22 23 | # airflow
|
||||
23 24 | DatasetFromRoot()
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.sdk import AssetAny
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 28 | DatasetFromRoot()
|
||||
--------------------------------------------------------------------------------
|
||||
26 27 | Dataset()
|
||||
27 28 | DatasetAlias()
|
||||
28 29 | DatasetAll()
|
||||
29 |-DatasetAny()
|
||||
30 |+AssetAny()
|
||||
30 31 | Metadata()
|
||||
31 32 | expand_alias_to_datasets()
|
||||
32 33 |
|
||||
30 31 | Dataset()
|
||||
31 32 | DatasetAlias()
|
||||
32 33 | DatasetAll()
|
||||
33 |-DatasetAny()
|
||||
34 |+AssetAny()
|
||||
34 35 | Metadata()
|
||||
35 36 | expand_alias_to_datasets()
|
||||
36 37 |
|
||||
|
||||
AIR311_names.py:30:1: AIR311 `airflow.datasets.metadata.Metadata` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:34:1: AIR311 `airflow.datasets.metadata.Metadata` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
28 | DatasetAll()
|
||||
29 | DatasetAny()
|
||||
30 | Metadata()
|
||||
32 | DatasetAll()
|
||||
33 | DatasetAny()
|
||||
34 | Metadata()
|
||||
| ^^^^^^^^ AIR311
|
||||
31 | expand_alias_to_datasets()
|
||||
35 | expand_alias_to_datasets()
|
||||
|
|
||||
= help: Use `airflow.sdk.Metadata` instead
|
||||
|
||||
AIR311_names.py:31:1: AIR311 [*] `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:35:1: AIR311 [*] `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
29 | DatasetAny()
|
||||
30 | Metadata()
|
||||
31 | expand_alias_to_datasets()
|
||||
33 | DatasetAny()
|
||||
34 | Metadata()
|
||||
35 | expand_alias_to_datasets()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ AIR311
|
||||
32 |
|
||||
33 | # airflow.decorators
|
||||
36 |
|
||||
37 | # airflow.decorators
|
||||
|
|
||||
= help: Use `airflow.sdk.expand_alias_to_assets` instead
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | from airflow.models.dag import DAG as DAGFromDag
|
||||
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
21 |+from airflow.sdk import expand_alias_to_assets
|
||||
21 22 |
|
||||
22 23 | # airflow
|
||||
23 24 | DatasetFromRoot()
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.sdk import expand_alias_to_assets
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 28 | DatasetFromRoot()
|
||||
--------------------------------------------------------------------------------
|
||||
28 29 | DatasetAll()
|
||||
29 30 | DatasetAny()
|
||||
30 31 | Metadata()
|
||||
31 |-expand_alias_to_datasets()
|
||||
32 |+expand_alias_to_assets()
|
||||
32 33 |
|
||||
33 34 | # airflow.decorators
|
||||
34 35 | dag()
|
||||
32 33 | DatasetAll()
|
||||
33 34 | DatasetAny()
|
||||
34 35 | Metadata()
|
||||
35 |-expand_alias_to_datasets()
|
||||
36 |+expand_alias_to_assets()
|
||||
36 37 |
|
||||
37 38 | # airflow.decorators
|
||||
38 39 | dag()
|
||||
|
||||
AIR311_names.py:34:1: AIR311 `airflow.decorators.dag` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:38:1: AIR311 `airflow.decorators.dag` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
33 | # airflow.decorators
|
||||
34 | dag()
|
||||
37 | # airflow.decorators
|
||||
38 | dag()
|
||||
| ^^^ AIR311
|
||||
35 | task()
|
||||
36 | task_group()
|
||||
39 | task()
|
||||
40 | task_group()
|
||||
|
|
||||
= help: Use `airflow.sdk.dag` instead
|
||||
|
||||
AIR311_names.py:35:1: AIR311 `airflow.decorators.task` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:39:1: AIR311 `airflow.decorators.task` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
33 | # airflow.decorators
|
||||
34 | dag()
|
||||
35 | task()
|
||||
37 | # airflow.decorators
|
||||
38 | dag()
|
||||
39 | task()
|
||||
| ^^^^ AIR311
|
||||
36 | task_group()
|
||||
37 | setup()
|
||||
40 | task_group()
|
||||
41 | setup()
|
||||
|
|
||||
= help: Use `airflow.sdk.task` instead
|
||||
|
||||
AIR311_names.py:36:1: AIR311 `airflow.decorators.task_group` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:40:1: AIR311 `airflow.decorators.task_group` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
34 | dag()
|
||||
35 | task()
|
||||
36 | task_group()
|
||||
38 | dag()
|
||||
39 | task()
|
||||
40 | task_group()
|
||||
| ^^^^^^^^^^ AIR311
|
||||
37 | setup()
|
||||
38 | teardown()
|
||||
41 | setup()
|
||||
42 | teardown()
|
||||
|
|
||||
= help: Use `airflow.sdk.task_group` instead
|
||||
|
||||
AIR311_names.py:37:1: AIR311 `airflow.decorators.setup` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:41:1: AIR311 `airflow.decorators.setup` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
35 | task()
|
||||
36 | task_group()
|
||||
37 | setup()
|
||||
39 | task()
|
||||
40 | task_group()
|
||||
41 | setup()
|
||||
| ^^^^^ AIR311
|
||||
38 | teardown()
|
||||
42 | teardown()
|
||||
|
|
||||
= help: Use `airflow.sdk.setup` instead
|
||||
|
||||
AIR311_names.py:38:1: AIR311 `airflow.decorators.teardown` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:42:1: AIR311 `airflow.decorators.teardown` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
36 | task_group()
|
||||
37 | setup()
|
||||
38 | teardown()
|
||||
40 | task_group()
|
||||
41 | setup()
|
||||
42 | teardown()
|
||||
| ^^^^^^^^ AIR311
|
||||
39 |
|
||||
40 | # airflow.io
|
||||
43 |
|
||||
44 | # airflow.io
|
||||
|
|
||||
= help: Use `airflow.sdk.teardown` instead
|
||||
|
||||
AIR311_names.py:41:1: AIR311 `airflow.io.path.ObjectStoragePath` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:45:1: AIR311 `airflow.io.path.ObjectStoragePath` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
40 | # airflow.io
|
||||
41 | ObjectStoragePath()
|
||||
44 | # airflow.io
|
||||
45 | ObjectStoragePath()
|
||||
| ^^^^^^^^^^^^^^^^^ AIR311
|
||||
42 | attach()
|
||||
46 | attach()
|
||||
|
|
||||
= help: Use `airflow.sdk.ObjectStoragePath` instead
|
||||
|
||||
AIR311_names.py:42:1: AIR311 `airflow.io.storage.attach` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:46:1: AIR311 `airflow.io.storage.attach` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
40 | # airflow.io
|
||||
41 | ObjectStoragePath()
|
||||
42 | attach()
|
||||
44 | # airflow.io
|
||||
45 | ObjectStoragePath()
|
||||
46 | attach()
|
||||
| ^^^^^^ AIR311
|
||||
43 |
|
||||
44 | # airflow.models
|
||||
47 |
|
||||
48 | # airflow.models
|
||||
|
|
||||
= help: Use `airflow.sdk.io.attach` instead
|
||||
|
||||
AIR311_names.py:45:1: AIR311 `airflow.models.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:49:1: AIR311 `airflow.models.Connection` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
44 | # airflow.models
|
||||
45 | DAGFromModel()
|
||||
48 | # airflow.models
|
||||
49 | Connection()
|
||||
| ^^^^^^^^^^ AIR311
|
||||
50 | DAGFromModel()
|
||||
51 | Variable()
|
||||
|
|
||||
= help: Use `airflow.sdk.Connection` instead
|
||||
|
||||
AIR311_names.py:50:1: AIR311 [*] `airflow.models.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
48 | # airflow.models
|
||||
49 | Connection()
|
||||
50 | DAGFromModel()
|
||||
| ^^^^^^^^^^^^ AIR311
|
||||
46 |
|
||||
47 | # airflow.models.baseoperator
|
||||
51 | Variable()
|
||||
|
|
||||
= help: Use `airflow.sdk.DAG` instead
|
||||
|
||||
AIR311_names.py:48:1: AIR311 `airflow.models.baseoperator.chain` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
ℹ Safe fix
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.sdk import DAG
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 28 | DatasetFromRoot()
|
||||
--------------------------------------------------------------------------------
|
||||
47 48 |
|
||||
48 49 | # airflow.models
|
||||
49 50 | Connection()
|
||||
50 |-DAGFromModel()
|
||||
51 |+DAG()
|
||||
51 52 | Variable()
|
||||
52 53 |
|
||||
53 54 | # airflow.models.baseoperator
|
||||
|
||||
AIR311_names.py:51:1: AIR311 `airflow.models.Variable` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
47 | # airflow.models.baseoperator
|
||||
48 | chain()
|
||||
49 | Connection()
|
||||
50 | DAGFromModel()
|
||||
51 | Variable()
|
||||
| ^^^^^^^^ AIR311
|
||||
52 |
|
||||
53 | # airflow.models.baseoperator
|
||||
|
|
||||
= help: Use `airflow.sdk.Variable` instead
|
||||
|
||||
AIR311_names.py:54:1: AIR311 `airflow.models.baseoperator.chain` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
53 | # airflow.models.baseoperator
|
||||
54 | chain()
|
||||
| ^^^^^ AIR311
|
||||
49 | chain_linear()
|
||||
50 | cross_downstream()
|
||||
55 | chain_linear()
|
||||
56 | cross_downstream()
|
||||
|
|
||||
= help: Use `airflow.sdk.chain` instead
|
||||
|
||||
AIR311_names.py:49:1: AIR311 `airflow.models.baseoperator.chain_linear` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:55:1: AIR311 `airflow.models.baseoperator.chain_linear` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
47 | # airflow.models.baseoperator
|
||||
48 | chain()
|
||||
49 | chain_linear()
|
||||
53 | # airflow.models.baseoperator
|
||||
54 | chain()
|
||||
55 | chain_linear()
|
||||
| ^^^^^^^^^^^^ AIR311
|
||||
50 | cross_downstream()
|
||||
56 | cross_downstream()
|
||||
|
|
||||
= help: Use `airflow.sdk.chain_linear` instead
|
||||
|
||||
AIR311_names.py:50:1: AIR311 `airflow.models.baseoperator.cross_downstream` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:56:1: AIR311 `airflow.models.baseoperator.cross_downstream` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
48 | chain()
|
||||
49 | chain_linear()
|
||||
50 | cross_downstream()
|
||||
54 | chain()
|
||||
55 | chain_linear()
|
||||
56 | cross_downstream()
|
||||
| ^^^^^^^^^^^^^^^^ AIR311
|
||||
51 |
|
||||
52 | # airflow.models.baseoperatolinker
|
||||
57 |
|
||||
58 | # airflow.models.baseoperatolinker
|
||||
|
|
||||
= help: Use `airflow.sdk.cross_downstream` instead
|
||||
|
||||
AIR311_names.py:56:1: AIR311 `airflow.models.dag.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:62:1: AIR311 [*] `airflow.models.dag.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
55 | # airflow.models.dag
|
||||
56 | DAGFromDag()
|
||||
61 | # airflow.models.dag
|
||||
62 | DAGFromDag()
|
||||
| ^^^^^^^^^^ AIR311
|
||||
57 | # airflow.timetables.datasets
|
||||
58 | DatasetOrTimeSchedule()
|
||||
63 | # airflow.timetables.datasets
|
||||
64 | DatasetOrTimeSchedule()
|
||||
|
|
||||
= help: Use `airflow.sdk.DAG` instead
|
||||
|
||||
AIR311_names.py:58:1: AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
ℹ Safe fix
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.sdk import DAG
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 28 | DatasetFromRoot()
|
||||
--------------------------------------------------------------------------------
|
||||
59 60 | BaseOperatorLink()
|
||||
60 61 |
|
||||
61 62 | # airflow.models.dag
|
||||
62 |-DAGFromDag()
|
||||
63 |+DAG()
|
||||
63 64 | # airflow.timetables.datasets
|
||||
64 65 | DatasetOrTimeSchedule()
|
||||
65 66 |
|
||||
|
||||
AIR311_names.py:64:1: AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
56 | DAGFromDag()
|
||||
57 | # airflow.timetables.datasets
|
||||
58 | DatasetOrTimeSchedule()
|
||||
62 | DAGFromDag()
|
||||
63 | # airflow.timetables.datasets
|
||||
64 | DatasetOrTimeSchedule()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR311
|
||||
59 |
|
||||
60 | # airflow.utils.dag_parsing_context
|
||||
65 |
|
||||
66 | # airflow.utils.dag_parsing_context
|
||||
|
|
||||
= help: Use `airflow.timetables.assets.AssetOrTimeSchedule` instead
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | from airflow.models.dag import DAG as DAGFromDag
|
||||
19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
20 20 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
21 |+from airflow.timetables.assets import AssetOrTimeSchedule
|
||||
21 22 |
|
||||
22 23 | # airflow
|
||||
23 24 | DatasetFromRoot()
|
||||
22 22 | from airflow.models.dag import DAG as DAGFromDag
|
||||
23 23 | from airflow.timetables.datasets import DatasetOrTimeSchedule
|
||||
24 24 | from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
25 |+from airflow.timetables.assets import AssetOrTimeSchedule
|
||||
25 26 |
|
||||
26 27 | # airflow
|
||||
27 28 | DatasetFromRoot()
|
||||
--------------------------------------------------------------------------------
|
||||
55 56 | # airflow.models.dag
|
||||
56 57 | DAGFromDag()
|
||||
57 58 | # airflow.timetables.datasets
|
||||
58 |-DatasetOrTimeSchedule()
|
||||
59 |+AssetOrTimeSchedule()
|
||||
59 60 |
|
||||
60 61 | # airflow.utils.dag_parsing_context
|
||||
61 62 | get_parsing_context()
|
||||
61 62 | # airflow.models.dag
|
||||
62 63 | DAGFromDag()
|
||||
63 64 | # airflow.timetables.datasets
|
||||
64 |-DatasetOrTimeSchedule()
|
||||
65 |+AssetOrTimeSchedule()
|
||||
65 66 |
|
||||
66 67 | # airflow.utils.dag_parsing_context
|
||||
67 68 | get_parsing_context()
|
||||
|
||||
AIR311_names.py:61:1: AIR311 `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
AIR311_names.py:67:1: AIR311 `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
|
|
||||
60 | # airflow.utils.dag_parsing_context
|
||||
61 | get_parsing_context()
|
||||
66 | # airflow.utils.dag_parsing_context
|
||||
67 | get_parsing_context()
|
||||
| ^^^^^^^^^^^^^^^^^^^ AIR311
|
||||
|
|
||||
= help: Use `airflow.sdk.get_parsing_context` instead
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
|
||||
@@ -100,7 +100,15 @@ pub(crate) fn hardcoded_sql_expression(checker: &Checker, expr: &Expr) {
|
||||
}
|
||||
|
||||
// f"select * from table where val = {val}"
|
||||
Expr::FString(f_string) => concatenated_f_string(f_string, checker.locator()),
|
||||
Expr::FString(f_string)
|
||||
if f_string
|
||||
.value
|
||||
.f_strings()
|
||||
.any(|fs| fs.elements.iter().any(ast::FStringElement::is_expression)) =>
|
||||
{
|
||||
concatenated_f_string(f_string, checker.locator())
|
||||
}
|
||||
|
||||
_ => return,
|
||||
};
|
||||
|
||||
|
||||
@@ -601,4 +601,6 @@ S608.py:164:11: S608 Possible SQL injection vector through string-based query co
|
||||
167 | | FROM ({user_input}) raw
|
||||
168 | | """
|
||||
| |___^ S608
|
||||
169 |
|
||||
170 | # https://github.com/astral-sh/ruff/issues/17967
|
||||
|
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{checkers::ast::Checker, fix::edits::add_argument};
|
||||
@@ -60,10 +60,18 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When prefixes are supplied, stacklevel is implicitly overridden to be `max(2, stacklevel)`.
|
||||
//
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/warnings.html#warnings.warn)
|
||||
// ```text
|
||||
// 0 1 2 3 4
|
||||
// warnings.warn(message, category=None, stacklevel=1, source=None, *, skip_file_prefixes=())
|
||||
// ```
|
||||
if call
|
||||
.arguments
|
||||
.find_argument_value("stacklevel", 2)
|
||||
.is_some()
|
||||
|| is_skip_file_prefixes_param_set(&call.arguments)
|
||||
|| call
|
||||
.arguments
|
||||
.args
|
||||
@@ -90,3 +98,14 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) {
|
||||
|
||||
checker.report_diagnostic(diagnostic);
|
||||
}
|
||||
|
||||
/// Returns `true` if `skip_file_prefixes` is set to its non-default value.
|
||||
/// The default value of `skip_file_prefixes` is an empty tuple.
|
||||
fn is_skip_file_prefixes_param_set(arguments: &ast::Arguments) -> bool {
|
||||
arguments
|
||||
.find_keyword("skip_file_prefixes")
|
||||
.is_some_and(|keyword| match &keyword.value {
|
||||
Expr::Tuple(tuple) => !tuple.elts.is_empty(),
|
||||
_ => true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,3 +61,26 @@ B028.py:22:1: B028 [*] No explicit `stacklevel` keyword argument found
|
||||
26 |- source = None # no trailing comma
|
||||
26 |+ source = None, stacklevel=2 # no trailing comma
|
||||
27 27 | )
|
||||
28 28 |
|
||||
29 29 | # https://github.com/astral-sh/ruff/issues/18011
|
||||
|
||||
B028.py:32:1: B028 [*] No explicit `stacklevel` keyword argument found
|
||||
|
|
||||
30 | warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),))
|
||||
31 | # trigger diagnostic if `skip_file_prefixes` is present and set to the default value
|
||||
32 | warnings.warn("test", skip_file_prefixes=())
|
||||
| ^^^^^^^^^^^^^ B028
|
||||
33 |
|
||||
34 | _my_prefixes = ("this","that")
|
||||
|
|
||||
= help: Set `stacklevel=2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
29 29 | # https://github.com/astral-sh/ruff/issues/18011
|
||||
30 30 | warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),))
|
||||
31 31 | # trigger diagnostic if `skip_file_prefixes` is present and set to the default value
|
||||
32 |-warnings.warn("test", skip_file_prefixes=())
|
||||
32 |+warnings.warn("test", skip_file_prefixes=(), stacklevel=2)
|
||||
33 33 |
|
||||
34 34 | _my_prefixes = ("this","that")
|
||||
35 35 | warnings.warn("test", skip_file_prefixes = _my_prefixes)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use itertools::Itertools;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
@@ -19,6 +19,7 @@ use crate::fix::edits::{remove_argument, Parentheses};
|
||||
/// arguments directly.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(bar):
|
||||
/// return bar + 1
|
||||
@@ -28,6 +29,7 @@ use crate::fix::edits::{remove_argument, Parentheses};
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(bar):
|
||||
/// return bar + 1
|
||||
@@ -36,6 +38,26 @@ use crate::fix::edits::{remove_argument, Parentheses};
|
||||
/// print(foo(bar=2)) # prints 3
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
///
|
||||
/// This rule's fix is marked as unsafe for dictionaries with comments interleaved between
|
||||
/// the items, as comments may be removed.
|
||||
///
|
||||
/// For example, the fix would be marked as unsafe in the following case:
|
||||
///
|
||||
/// ```python
|
||||
/// foo(
|
||||
/// **{
|
||||
/// # comment
|
||||
/// "x": 1.0,
|
||||
/// # comment
|
||||
/// "y": 2.0,
|
||||
/// }
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// as this is converted to `foo(x=1.0, y=2.0)` without any of the comments.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays)
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
@@ -113,7 +135,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
|
||||
.iter()
|
||||
.all(|kwarg| !duplicate_keywords.contains(kwarg))
|
||||
{
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
let edit = Edit::range_replacement(
|
||||
kwargs
|
||||
.iter()
|
||||
.zip(dict.iter_values())
|
||||
@@ -134,7 +156,15 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
|
||||
})
|
||||
.join(", "),
|
||||
keyword.range(),
|
||||
)));
|
||||
);
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
edit,
|
||||
if checker.comment_ranges().intersects(dict.range()) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,8 @@ PIE804.py:29:12: PIE804 [*] Unnecessary `dict` kwargs
|
||||
29 |-abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
29 |+abc(foo=1, bar=(bar := 1)) # PIE804
|
||||
30 30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
31 31 |
|
||||
32 32 | # https://github.com/astral-sh/ruff/issues/18036
|
||||
|
||||
PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
@@ -215,6 +217,8 @@ PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
|
||||
29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PIE804
|
||||
31 |
|
||||
32 | # https://github.com/astral-sh/ruff/issues/18036
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
@@ -224,3 +228,34 @@ PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
|
||||
29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
30 |-abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
30 |+abc(foo=1, bar=(yield 1)) # PIE804
|
||||
31 31 |
|
||||
32 32 | # https://github.com/astral-sh/ruff/issues/18036
|
||||
33 33 | # The autofix for this is unsafe due to the comments inside the dictionary.
|
||||
|
||||
PIE804.py:35:5: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
33 | # The autofix for this is unsafe due to the comments inside the dictionary.
|
||||
34 | foo(
|
||||
35 | / **{
|
||||
36 | | # Comment 1
|
||||
37 | | "x": 1.0,
|
||||
38 | | # Comment 2
|
||||
39 | | "y": 2.0,
|
||||
40 | | }
|
||||
| |_____^ PIE804
|
||||
41 | )
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
ℹ Unsafe fix
|
||||
32 32 | # https://github.com/astral-sh/ruff/issues/18036
|
||||
33 33 | # The autofix for this is unsafe due to the comments inside the dictionary.
|
||||
34 34 | foo(
|
||||
35 |- **{
|
||||
36 |- # Comment 1
|
||||
37 |- "x": 1.0,
|
||||
38 |- # Comment 2
|
||||
39 |- "y": 2.0,
|
||||
40 |- }
|
||||
35 |+ x=1.0, y=2.0
|
||||
41 36 | )
|
||||
|
||||
@@ -186,12 +186,20 @@ fn split_sep(
|
||||
let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) {
|
||||
match direction {
|
||||
Direction::Left => value.splitn(split_n + 1, sep_value).collect(),
|
||||
Direction::Right => value.rsplitn(split_n + 1, sep_value).collect(),
|
||||
Direction::Right => {
|
||||
let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect();
|
||||
items.reverse();
|
||||
items
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match direction {
|
||||
Direction::Left => value.split(sep_value).collect(),
|
||||
Direction::Right => value.rsplit(sep_value).collect(),
|
||||
Direction::Right => {
|
||||
let mut items: Vec<&str> = value.rsplit(sep_value).collect();
|
||||
items.reverse();
|
||||
items
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ SIM905.py:32:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
|
||||
33 | "".split() # []
|
||||
34 | """
|
||||
34 | """
|
||||
|
|
||||
= help: Replace with list literal
|
||||
|
||||
@@ -363,7 +363,7 @@ SIM905.py:32:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
32 |-" a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
||||
32 |+[" a", "a a", "a a "] # [" a", "a a", "a a "]
|
||||
33 33 | "".split() # []
|
||||
34 34 | """
|
||||
34 34 | """
|
||||
35 35 | """.split() # []
|
||||
|
||||
SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
@@ -371,7 +371,7 @@ SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
||||
33 | "".split() # []
|
||||
| ^^^^^^^^^^ SIM905
|
||||
34 | """
|
||||
34 | """
|
||||
35 | """.split() # []
|
||||
|
|
||||
= help: Replace with list literal
|
||||
@@ -382,7 +382,7 @@ SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
||||
33 |-"".split() # []
|
||||
33 |+[] # []
|
||||
34 34 | """
|
||||
34 34 | """
|
||||
35 35 | """.split() # []
|
||||
36 36 | " ".split() # []
|
||||
|
||||
@@ -390,7 +390,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
||||
33 | "".split() # []
|
||||
34 | / """
|
||||
34 | / """
|
||||
35 | | """.split() # []
|
||||
| |___________^ SIM905
|
||||
36 | " ".split() # []
|
||||
@@ -402,7 +402,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
31 31 |
|
||||
32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
||||
33 33 | "".split() # []
|
||||
34 |-"""
|
||||
34 |-"""
|
||||
35 |-""".split() # []
|
||||
34 |+[] # []
|
||||
36 35 | " ".split() # []
|
||||
@@ -411,7 +411,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
||||
SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
34 | """
|
||||
34 | """
|
||||
35 | """.split() # []
|
||||
36 | " ".split() # []
|
||||
| ^^^^^^^^^^^^^^^^^ SIM905
|
||||
@@ -422,7 +422,7 @@ SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
||||
ℹ Safe fix
|
||||
33 33 | "".split() # []
|
||||
34 34 | """
|
||||
34 34 | """
|
||||
35 35 | """.split() # []
|
||||
36 |-" ".split() # []
|
||||
36 |+[] # []
|
||||
@@ -442,7 +442,7 @@ SIM905.py:37:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
= help: Replace with list literal
|
||||
|
||||
ℹ Safe fix
|
||||
34 34 | """
|
||||
34 34 | """
|
||||
35 35 | """.split() # []
|
||||
36 36 | " ".split() # []
|
||||
37 |-"/abc/".split() # ["/abc/"]
|
||||
@@ -854,6 +854,8 @@ SIM905.py:103:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
107 | | "'itemD'"
|
||||
108 | | """.split()
|
||||
| |___________^ SIM905
|
||||
109 |
|
||||
110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
|
|
||||
= help: Replace with list literal
|
||||
|
||||
@@ -868,3 +870,39 @@ SIM905.py:103:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
107 |-"'itemD'"
|
||||
108 |-""".split()
|
||||
103 |+['"itemA"', "'itemB'", "'''itemC'''", "\"'itemD'\""]
|
||||
109 104 |
|
||||
110 105 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
111 106 | print("a,b".rsplit(","))
|
||||
|
||||
SIM905.py:111:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
111 | print("a,b".rsplit(","))
|
||||
| ^^^^^^^^^^^^^^^^^ SIM905
|
||||
112 | print("a,b,c".rsplit(",", 1))
|
||||
|
|
||||
= help: Replace with list literal
|
||||
|
||||
ℹ Safe fix
|
||||
108 108 | """.split()
|
||||
109 109 |
|
||||
110 110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
111 |-print("a,b".rsplit(","))
|
||||
111 |+print(["a", "b"])
|
||||
112 112 | print("a,b,c".rsplit(",", 1))
|
||||
|
||||
SIM905.py:112:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
111 | print("a,b".rsplit(","))
|
||||
112 | print("a,b,c".rsplit(",", 1))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ SIM905
|
||||
|
|
||||
= help: Replace with list literal
|
||||
|
||||
ℹ Safe fix
|
||||
109 109 |
|
||||
110 110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
111 111 | print("a,b".rsplit(","))
|
||||
112 |-print("a,b,c".rsplit(",", 1))
|
||||
112 |+print(["a,b", "c"])
|
||||
|
||||
@@ -26,41 +26,109 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
// PTH100
|
||||
["os", "path", "abspath"] => OsPathAbspath.into(),
|
||||
// PTH101
|
||||
["os", "chmod"] => OsChmod.into(),
|
||||
["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_argument_non_default(&call.arguments, "dir_fd", 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
OsChmod.into()
|
||||
}
|
||||
// PTH102
|
||||
["os", "makedirs"] => OsMakedirs.into(),
|
||||
// PTH103
|
||||
["os", "mkdir"] => OsMkdir.into(),
|
||||
["os", "mkdir"] => {
|
||||
// `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.mkdir)
|
||||
// ```text
|
||||
// 0 1 2
|
||||
// os.mkdir(path, mode=0o777, *, dir_fd=None)
|
||||
// ```
|
||||
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
|
||||
return;
|
||||
}
|
||||
OsMkdir.into()
|
||||
}
|
||||
// PTH104
|
||||
["os", "rename"] => {
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// are set to non-default values.
|
||||
// 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 call
|
||||
.arguments
|
||||
.find_argument_value("src_dir_fd", 2)
|
||||
.is_some_and(|expr| !expr.is_none_literal_expr())
|
||||
|| call
|
||||
.arguments
|
||||
.find_argument_value("dst_dir_fd", 3)
|
||||
.is_some_and(|expr| !expr.is_none_literal_expr())
|
||||
if is_argument_non_default(&call.arguments, "src_dir_fd", 2)
|
||||
|| is_argument_non_default(&call.arguments, "dst_dir_fd", 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
OsRename.into()
|
||||
}
|
||||
// PTH105
|
||||
["os", "replace"] => OsReplace.into(),
|
||||
["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_argument_non_default(&call.arguments, "src_dir_fd", 2)
|
||||
|| is_argument_non_default(&call.arguments, "dst_dir_fd", 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
OsReplace.into()
|
||||
}
|
||||
// PTH106
|
||||
["os", "rmdir"] => OsRmdir.into(),
|
||||
["os", "rmdir"] => {
|
||||
// `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.rmdir)
|
||||
// ```text
|
||||
// 0 1
|
||||
// os.rmdir(path, *, dir_fd=None)
|
||||
// ```
|
||||
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
|
||||
return;
|
||||
}
|
||||
OsRmdir.into()
|
||||
}
|
||||
// PTH107
|
||||
["os", "remove"] => OsRemove.into(),
|
||||
["os", "remove"] => {
|
||||
// `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.remove)
|
||||
// ```text
|
||||
// 0 1
|
||||
// os.remove(path, *, dir_fd=None)
|
||||
// ```
|
||||
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
|
||||
return;
|
||||
}
|
||||
OsRemove.into()
|
||||
}
|
||||
// PTH108
|
||||
["os", "unlink"] => OsUnlink.into(),
|
||||
["os", "unlink"] => {
|
||||
// `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.unlink)
|
||||
// ```text
|
||||
// 0 1
|
||||
// os.unlink(path, *, dir_fd=None)
|
||||
// ```
|
||||
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
|
||||
return;
|
||||
}
|
||||
OsUnlink.into()
|
||||
}
|
||||
// PTH109
|
||||
["os", "getcwd"] => OsGetcwd.into(),
|
||||
["os", "getcwdb"] => OsGetcwd.into(),
|
||||
@@ -76,10 +144,17 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
["os", "path", "islink"] => OsPathIslink.into(),
|
||||
// PTH116
|
||||
["os", "stat"] => {
|
||||
// `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.stat)
|
||||
// ```text
|
||||
// 0 1 2
|
||||
// os.stat(path, *, dir_fd=None, follow_symlinks=True)
|
||||
// ```
|
||||
if call
|
||||
.arguments
|
||||
.find_positional(0)
|
||||
.find_argument_value("path", 0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
|| is_argument_non_default(&call.arguments, "dir_fd", 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -148,13 +223,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. })
|
||||
)
|
||||
})
|
||||
|| is_argument_non_default(&call.arguments, "opener", 7)
|
||||
|| call
|
||||
.arguments
|
||||
.find_argument_value("opener", 7)
|
||||
.is_some_and(|expr| !expr.is_none_literal_expr())
|
||||
|| call
|
||||
.arguments
|
||||
.find_positional(0)
|
||||
.find_argument_value("file", 0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
{
|
||||
return;
|
||||
@@ -164,17 +236,53 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
// PTH124
|
||||
["py", "path", "local"] => PyPath.into(),
|
||||
// PTH207
|
||||
["glob", "glob"] => Glob {
|
||||
function: "glob".to_string(),
|
||||
["glob", "glob"] => {
|
||||
// `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/glob.html#glob.glob)
|
||||
// ```text
|
||||
// 0 1 2 3 4
|
||||
// glob.glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
|
||||
// ```
|
||||
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
Glob {
|
||||
function: "glob".to_string(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
.into(),
|
||||
["glob", "iglob"] => Glob {
|
||||
function: "iglob".to_string(),
|
||||
|
||||
["glob", "iglob"] => {
|
||||
// `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/glob.html#glob.iglob)
|
||||
// ```text
|
||||
// 0 1 2 3 4
|
||||
// glob.iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
|
||||
// ```
|
||||
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
Glob {
|
||||
function: "iglob".to_string(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
.into(),
|
||||
// PTH115
|
||||
// Python 3.9+
|
||||
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => OsReadlink.into(),
|
||||
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => {
|
||||
// `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.readlink)
|
||||
// ```text
|
||||
// 0 1
|
||||
// os.readlink(path, *, dir_fd=None)
|
||||
// ```
|
||||
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
|
||||
return;
|
||||
}
|
||||
OsReadlink.into()
|
||||
}
|
||||
// PTH208
|
||||
["os", "listdir"] => {
|
||||
if call
|
||||
@@ -224,3 +332,10 @@ fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
|
||||
_ => 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
|
||||
.find_argument_value(name, position)
|
||||
.is_some_and(|expr| !expr.is_none_literal_expr())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PTH207.py:9:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob`
|
||||
|
|
||||
@@ -26,4 +25,6 @@ PTH207.py:11:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob`
|
||||
10 | list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp")))
|
||||
11 | search("*.png")
|
||||
| ^^^^^^ PTH207
|
||||
12 |
|
||||
13 | # if `dir_fd` is set, suppress the diagnostic
|
||||
|
|
||||
|
||||
@@ -93,7 +93,7 @@ PERF401.py:142:9: PERF401 Use a list comprehension to create a transformed list
|
||||
|
||||
PERF401.py:149:9: PERF401 Use a list comprehension to create a transformed list
|
||||
|
|
||||
147 | tmp = 1; result = [] # commment should be protected
|
||||
147 | tmp = 1; result = [] # comment should be protected
|
||||
148 | for i in range(10):
|
||||
149 | result.append(i + 1) # PERF401
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
@@ -102,7 +102,7 @@ PERF401.py:149:9: PERF401 Use a list comprehension to create a transformed list
|
||||
|
||||
PERF401.py:156:9: PERF401 Use a list comprehension to create a transformed list
|
||||
|
|
||||
154 | result = []; tmp = 1 # commment should be protected
|
||||
154 | result = []; tmp = 1 # comment should be protected
|
||||
155 | for i in range(10):
|
||||
156 | result.append(i + 1) # PERF401
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
|
||||
@@ -223,7 +223,7 @@ PERF401.py:142:9: PERF401 [*] Use a list comprehension to create a transformed l
|
||||
|
||||
PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed list
|
||||
|
|
||||
147 | tmp = 1; result = [] # commment should be protected
|
||||
147 | tmp = 1; result = [] # comment should be protected
|
||||
148 | for i in range(10):
|
||||
149 | result.append(i + 1) # PERF401
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
@@ -234,10 +234,10 @@ PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed l
|
||||
144 144 |
|
||||
145 145 | def f():
|
||||
146 146 | # make sure that `tmp` is not deleted
|
||||
147 |- tmp = 1; result = [] # commment should be protected
|
||||
147 |- tmp = 1; result = [] # comment should be protected
|
||||
148 |- for i in range(10):
|
||||
149 |- result.append(i + 1) # PERF401
|
||||
147 |+ tmp = 1 # commment should be protected
|
||||
147 |+ tmp = 1 # comment should be protected
|
||||
148 |+ result = [i + 1 for i in range(10)] # PERF401
|
||||
150 149 |
|
||||
151 150 |
|
||||
@@ -245,7 +245,7 @@ PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed l
|
||||
|
||||
PERF401.py:156:9: PERF401 [*] Use a list comprehension to create a transformed list
|
||||
|
|
||||
154 | result = []; tmp = 1 # commment should be protected
|
||||
154 | result = []; tmp = 1 # comment should be protected
|
||||
155 | for i in range(10):
|
||||
156 | result.append(i + 1) # PERF401
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
@@ -256,10 +256,10 @@ PERF401.py:156:9: PERF401 [*] Use a list comprehension to create a transformed l
|
||||
151 151 |
|
||||
152 152 | def f():
|
||||
153 153 | # make sure that `tmp` is not deleted
|
||||
154 |- result = []; tmp = 1 # commment should be protected
|
||||
154 |- result = []; tmp = 1 # comment should be protected
|
||||
155 |- for i in range(10):
|
||||
156 |- result.append(i + 1) # PERF401
|
||||
154 |+ tmp = 1 # commment should be protected
|
||||
154 |+ tmp = 1 # comment should be protected
|
||||
155 |+ result = [i + 1 for i in range(10)] # PERF401
|
||||
157 156 |
|
||||
158 157 |
|
||||
|
||||
@@ -38,6 +38,11 @@ use crate::checkers::ast::Checker;
|
||||
/// nums.add(num + 5)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This fix is always unsafe because it changes the program’s behavior. Replacing the
|
||||
/// original set with a copy during iteration allows code that would previously raise a
|
||||
/// `RuntimeError` to run without error.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set)
|
||||
#[derive(ViolationMetadata)]
|
||||
|
||||
@@ -21,6 +21,7 @@ pub(crate) enum MinMax {
|
||||
/// readability.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// minimum = min(1, 2, min(3, 4, 5))
|
||||
/// maximum = max(1, 2, max(3, 4, 5))
|
||||
@@ -28,12 +29,26 @@ pub(crate) enum MinMax {
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// minimum = min(1, 2, 3, 4, 5)
|
||||
/// maximum = max(1, 2, 3, 4, 5)
|
||||
/// diff = maximum - minimum
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
///
|
||||
/// This fix is always unsafe and may change the program's behavior for types without full
|
||||
/// equivalence relations, such as float comparisons involving `NaN`.
|
||||
///
|
||||
/// ```python
|
||||
/// print(min(2.0, min(float("nan"), 1.0))) # before fix: 2.0
|
||||
/// print(min(2.0, float("nan"), 1.0)) # after fix: 1.0
|
||||
///
|
||||
/// print(max(1.0, max(float("nan"), 2.0))) # before fix: 1.0
|
||||
/// print(max(1.0, float("nan"), 2.0)) # after fix: 2.0
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min)
|
||||
/// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max)
|
||||
|
||||
@@ -28,6 +28,17 @@ use crate::fix::edits::add_argument;
|
||||
/// Python 3.10 and later, or `locale.getpreferredencoding()` on earlier versions,
|
||||
/// to make the encoding explicit.
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This fix is always unsafe and may change the program's behavior. It forces
|
||||
/// `encoding="utf-8"` as the default, regardless of the platform’s actual default
|
||||
/// encoding, which may cause `UnicodeDecodeError` on non-UTF-8 systems.
|
||||
/// ```python
|
||||
/// with open("test.txt") as f:
|
||||
/// print(f.read()) # before fix (on UTF-8 systems): 你好,世界!
|
||||
/// with open("test.txt", encoding="utf-8") as f:
|
||||
/// print(f.read()) # after fix (on Windows): UnicodeDecodeError
|
||||
/// ```
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// open("file.txt")
|
||||
|
||||
@@ -12,6 +12,11 @@ use crate::checkers::ast::Checker;
|
||||
/// ## Why is this bad?
|
||||
/// The import alias is redundant and should be removed to avoid confusion.
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This fix is marked as always unsafe because the user may be intentionally
|
||||
/// re-exporting the import. While statements like `import numpy as numpy`
|
||||
/// appear redundant, they can have semantic meaning in certain contexts.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import numpy as numpy
|
||||
|
||||
@@ -89,4 +89,15 @@ mod tests {
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fstring_number_format_python_311() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("refurb/FURB116.py"),
|
||||
&settings::LinterSettings::for_rule(Rule::FStringNumberFormat)
|
||||
.with_target_version(PythonVersion::PY311),
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall, Number};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall, Number, PythonVersion, UnaryOp};
|
||||
use ruff_source_file::find_newline;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -24,6 +25,11 @@ use crate::fix::snippet::SourceCodeSnippet;
|
||||
/// ```python
|
||||
/// print(f"{1337:b}")
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// The fix is only marked as safe for integer literals, all other cases
|
||||
/// are display-only, as they may change the runtime behaviour of the program
|
||||
/// or introduce syntax errors.
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct FStringNumberFormat {
|
||||
replacement: Option<SourceCodeSnippet>,
|
||||
@@ -121,21 +127,24 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a replacement, if possible.
|
||||
let replacement = if matches!(
|
||||
arg,
|
||||
Expr::NumberLiteral(_) | Expr::Name(_) | Expr::Attribute(_)
|
||||
) {
|
||||
let inner_source = checker.locator().slice(arg);
|
||||
|
||||
let quote = checker.stylist().quote();
|
||||
let shorthand = base.shorthand();
|
||||
|
||||
Some(format!("f{quote}{{{inner_source}:{shorthand}}}{quote}"))
|
||||
let maybe_number = if let Some(maybe_number) = arg
|
||||
.as_unary_op_expr()
|
||||
.filter(|unary_expr| unary_expr.op == UnaryOp::UAdd)
|
||||
.map(|unary_expr| &unary_expr.operand)
|
||||
{
|
||||
maybe_number
|
||||
} else {
|
||||
None
|
||||
arg
|
||||
};
|
||||
|
||||
let applicability = if matches!(maybe_number, Expr::NumberLiteral(_)) {
|
||||
Applicability::Safe
|
||||
} else {
|
||||
Applicability::DisplayOnly
|
||||
};
|
||||
|
||||
let replacement = try_create_replacement(checker, arg, base);
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
FStringNumberFormat {
|
||||
replacement: replacement.as_deref().map(SourceCodeSnippet::from_str),
|
||||
@@ -145,15 +154,54 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs
|
||||
);
|
||||
|
||||
if let Some(replacement) = replacement {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
replacement,
|
||||
subscript.range(),
|
||||
)));
|
||||
let edit = Edit::range_replacement(replacement, subscript.range());
|
||||
diagnostic.set_fix(Fix::applicable_edit(edit, applicability));
|
||||
}
|
||||
|
||||
checker.report_diagnostic(diagnostic);
|
||||
}
|
||||
|
||||
/// Generate a replacement, if possible.
|
||||
fn try_create_replacement(checker: &Checker, arg: &Expr, base: Base) -> Option<String> {
|
||||
if !matches!(
|
||||
arg,
|
||||
Expr::NumberLiteral(_) | Expr::Name(_) | Expr::Attribute(_) | Expr::UnaryOp(_)
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let inner_source = checker.locator().slice(arg);
|
||||
|
||||
// On Python 3.11 and earlier, trying to replace an `arg` that contains a backslash
|
||||
// would create a `SyntaxError` in the f-string.
|
||||
if checker.target_version() <= PythonVersion::PY311 && inner_source.contains('\\') {
|
||||
return None;
|
||||
}
|
||||
|
||||
// On Python 3.11 and earlier, trying to replace an `arg` that spans multiple lines
|
||||
// would create a `SyntaxError` in the f-string.
|
||||
if checker.target_version() <= PythonVersion::PY311 && find_newline(inner_source).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let quote = checker.stylist().quote();
|
||||
let shorthand = base.shorthand();
|
||||
|
||||
// If the `arg` contains double quotes we need to create the f-string with single quotes
|
||||
// to avoid a `SyntaxError` in Python 3.11 and earlier.
|
||||
if checker.target_version() <= PythonVersion::PY311 && inner_source.contains(quote.as_str()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If the `arg` contains a brace add an space before it to avoid a `SyntaxError`
|
||||
// in the f-string.
|
||||
if inner_source.starts_with('{') {
|
||||
Some(format!("f{quote}{{ {inner_source}:{shorthand}}}{quote}"))
|
||||
} else {
|
||||
Some(format!("f{quote}{{{inner_source}:{shorthand}}}{quote}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum Base {
|
||||
Hex,
|
||||
|
||||
@@ -1,144 +1,288 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB116.py:6:7: FURB116 [*] Replace `oct` call with `f"{num:o}"`
|
||||
|
|
||||
4 | return num
|
||||
5 |
|
||||
6 | print(oct(num)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^ FURB116
|
||||
7 | print(hex(num)[2:]) # FURB116
|
||||
8 | print(bin(num)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{num:o}"`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | def return_num() -> int:
|
||||
4 4 | return num
|
||||
5 5 |
|
||||
6 |-print(oct(num)[2:]) # FURB116
|
||||
6 |+print(f"{num:o}") # FURB116
|
||||
7 7 | print(hex(num)[2:]) # FURB116
|
||||
8 8 | print(bin(num)[2:]) # FURB116
|
||||
9 9 |
|
||||
|
||||
FURB116.py:7:7: FURB116 [*] Replace `hex` call with `f"{num:x}"`
|
||||
|
|
||||
6 | print(oct(num)[2:]) # FURB116
|
||||
7 | print(hex(num)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^ FURB116
|
||||
8 | print(bin(num)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{num:x}"`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | return num
|
||||
5 5 |
|
||||
6 6 | print(oct(num)[2:]) # FURB116
|
||||
7 |-print(hex(num)[2:]) # FURB116
|
||||
7 |+print(f"{num:x}") # FURB116
|
||||
8 8 | print(bin(num)[2:]) # FURB116
|
||||
9 9 |
|
||||
10 10 | print(oct(1337)[2:]) # FURB116
|
||||
|
||||
FURB116.py:8:7: FURB116 [*] Replace `bin` call with `f"{num:b}"`
|
||||
FURB116.py:9:7: FURB116 Replace `oct` call with `f"{num:o}"`
|
||||
|
|
||||
6 | print(oct(num)[2:]) # FURB116
|
||||
7 | print(hex(num)[2:]) # FURB116
|
||||
8 | print(bin(num)[2:]) # FURB116
|
||||
7 | return num
|
||||
8 |
|
||||
9 | print(oct(num)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^ FURB116
|
||||
9 |
|
||||
10 | print(oct(1337)[2:]) # FURB116
|
||||
10 | print(hex(num)[2:]) # FURB116
|
||||
11 | print(bin(num)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{num:o}"`
|
||||
|
||||
ℹ Display-only fix
|
||||
6 6 | def return_num() -> int:
|
||||
7 7 | return num
|
||||
8 8 |
|
||||
9 |-print(oct(num)[2:]) # FURB116
|
||||
9 |+print(f"{num:o}") # FURB116
|
||||
10 10 | print(hex(num)[2:]) # FURB116
|
||||
11 11 | print(bin(num)[2:]) # FURB116
|
||||
12 12 |
|
||||
|
||||
FURB116.py:10:7: FURB116 Replace `hex` call with `f"{num:x}"`
|
||||
|
|
||||
9 | print(oct(num)[2:]) # FURB116
|
||||
10 | print(hex(num)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^ FURB116
|
||||
11 | print(bin(num)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{num:x}"`
|
||||
|
||||
ℹ Display-only fix
|
||||
7 7 | return num
|
||||
8 8 |
|
||||
9 9 | print(oct(num)[2:]) # FURB116
|
||||
10 |-print(hex(num)[2:]) # FURB116
|
||||
10 |+print(f"{num:x}") # FURB116
|
||||
11 11 | print(bin(num)[2:]) # FURB116
|
||||
12 12 |
|
||||
13 13 | print(oct(1337)[2:]) # FURB116
|
||||
|
||||
FURB116.py:11:7: FURB116 Replace `bin` call with `f"{num:b}"`
|
||||
|
|
||||
9 | print(oct(num)[2:]) # FURB116
|
||||
10 | print(hex(num)[2:]) # FURB116
|
||||
11 | print(bin(num)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^ FURB116
|
||||
12 |
|
||||
13 | print(oct(1337)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{num:b}"`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 |
|
||||
6 6 | print(oct(num)[2:]) # FURB116
|
||||
7 7 | print(hex(num)[2:]) # FURB116
|
||||
8 |-print(bin(num)[2:]) # FURB116
|
||||
8 |+print(f"{num:b}") # FURB116
|
||||
9 9 |
|
||||
10 10 | print(oct(1337)[2:]) # FURB116
|
||||
11 11 | print(hex(1337)[2:]) # FURB116
|
||||
ℹ Display-only fix
|
||||
8 8 |
|
||||
9 9 | print(oct(num)[2:]) # FURB116
|
||||
10 10 | print(hex(num)[2:]) # FURB116
|
||||
11 |-print(bin(num)[2:]) # FURB116
|
||||
11 |+print(f"{num:b}") # FURB116
|
||||
12 12 |
|
||||
13 13 | print(oct(1337)[2:]) # FURB116
|
||||
14 14 | print(hex(1337)[2:]) # FURB116
|
||||
|
||||
FURB116.py:10:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
|
||||
FURB116.py:13:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
|
||||
|
|
||||
8 | print(bin(num)[2:]) # FURB116
|
||||
9 |
|
||||
10 | print(oct(1337)[2:]) # FURB116
|
||||
11 | print(bin(num)[2:]) # FURB116
|
||||
12 |
|
||||
13 | print(oct(1337)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^^ FURB116
|
||||
11 | print(hex(1337)[2:]) # FURB116
|
||||
12 | print(bin(1337)[2:]) # FURB116
|
||||
14 | print(hex(1337)[2:]) # FURB116
|
||||
15 | print(bin(1337)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{1337:o}"`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | print(hex(num)[2:]) # FURB116
|
||||
8 8 | print(bin(num)[2:]) # FURB116
|
||||
9 9 |
|
||||
10 |-print(oct(1337)[2:]) # FURB116
|
||||
10 |+print(f"{1337:o}") # FURB116
|
||||
11 11 | print(hex(1337)[2:]) # FURB116
|
||||
12 12 | print(bin(1337)[2:]) # FURB116
|
||||
13 13 |
|
||||
10 10 | print(hex(num)[2:]) # FURB116
|
||||
11 11 | print(bin(num)[2:]) # FURB116
|
||||
12 12 |
|
||||
13 |-print(oct(1337)[2:]) # FURB116
|
||||
13 |+print(f"{1337:o}") # FURB116
|
||||
14 14 | print(hex(1337)[2:]) # FURB116
|
||||
15 15 | print(bin(1337)[2:]) # FURB116
|
||||
16 16 | print(bin(+1337)[2:]) # FURB116
|
||||
|
||||
FURB116.py:11:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
|
||||
FURB116.py:14:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
|
||||
|
|
||||
10 | print(oct(1337)[2:]) # FURB116
|
||||
11 | print(hex(1337)[2:]) # FURB116
|
||||
13 | print(oct(1337)[2:]) # FURB116
|
||||
14 | print(hex(1337)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^^ FURB116
|
||||
12 | print(bin(1337)[2:]) # FURB116
|
||||
15 | print(bin(1337)[2:]) # FURB116
|
||||
16 | print(bin(+1337)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{1337:x}"`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | print(bin(num)[2:]) # FURB116
|
||||
9 9 |
|
||||
10 10 | print(oct(1337)[2:]) # FURB116
|
||||
11 |-print(hex(1337)[2:]) # FURB116
|
||||
11 |+print(f"{1337:x}") # FURB116
|
||||
12 12 | print(bin(1337)[2:]) # FURB116
|
||||
13 13 |
|
||||
14 14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
11 11 | print(bin(num)[2:]) # FURB116
|
||||
12 12 |
|
||||
13 13 | print(oct(1337)[2:]) # FURB116
|
||||
14 |-print(hex(1337)[2:]) # FURB116
|
||||
14 |+print(f"{1337:x}") # FURB116
|
||||
15 15 | print(bin(1337)[2:]) # FURB116
|
||||
16 16 | print(bin(+1337)[2:]) # FURB116
|
||||
17 17 |
|
||||
|
||||
FURB116.py:12:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
|
||||
FURB116.py:15:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
|
||||
|
|
||||
10 | print(oct(1337)[2:]) # FURB116
|
||||
11 | print(hex(1337)[2:]) # FURB116
|
||||
12 | print(bin(1337)[2:]) # FURB116
|
||||
13 | print(oct(1337)[2:]) # FURB116
|
||||
14 | print(hex(1337)[2:]) # FURB116
|
||||
15 | print(bin(1337)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^^ FURB116
|
||||
13 |
|
||||
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
16 | print(bin(+1337)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{1337:b}"`
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 |
|
||||
10 10 | print(oct(1337)[2:]) # FURB116
|
||||
11 11 | print(hex(1337)[2:]) # FURB116
|
||||
12 |-print(bin(1337)[2:]) # FURB116
|
||||
12 |+print(f"{1337:b}") # FURB116
|
||||
13 13 |
|
||||
14 14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
15 15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
12 12 |
|
||||
13 13 | print(oct(1337)[2:]) # FURB116
|
||||
14 14 | print(hex(1337)[2:]) # FURB116
|
||||
15 |-print(bin(1337)[2:]) # FURB116
|
||||
15 |+print(f"{1337:b}") # FURB116
|
||||
16 16 | print(bin(+1337)[2:]) # FURB116
|
||||
17 17 |
|
||||
18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
|
||||
FURB116.py:14:7: FURB116 Replace `bin` call with f-string
|
||||
FURB116.py:16:7: FURB116 [*] Replace `bin` call with `f"{+1337:b}"`
|
||||
|
|
||||
12 | print(bin(1337)[2:]) # FURB116
|
||||
13 |
|
||||
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
14 | print(hex(1337)[2:]) # FURB116
|
||||
15 | print(bin(1337)[2:]) # FURB116
|
||||
16 | print(bin(+1337)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^^^ FURB116
|
||||
17 |
|
||||
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
|
|
||||
= help: Replace with `f"{+1337:b}"`
|
||||
|
||||
ℹ Safe fix
|
||||
13 13 | print(oct(1337)[2:]) # FURB116
|
||||
14 14 | print(hex(1337)[2:]) # FURB116
|
||||
15 15 | print(bin(1337)[2:]) # FURB116
|
||||
16 |-print(bin(+1337)[2:]) # FURB116
|
||||
16 |+print(f"{+1337:b}") # FURB116
|
||||
17 17 |
|
||||
18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
19 19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
|
||||
FURB116.py:18:7: FURB116 Replace `bin` call with f-string
|
||||
|
|
||||
16 | print(bin(+1337)[2:]) # FURB116
|
||||
17 |
|
||||
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ FURB116
|
||||
15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
|
|
||||
= help: Replace with f-string
|
||||
|
||||
FURB116.py:15:7: FURB116 Replace `bin` call with f-string
|
||||
FURB116.py:19:7: FURB116 Replace `bin` call with f-string
|
||||
|
|
||||
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ FURB116
|
||||
16 |
|
||||
17 | ## invalid
|
||||
20 |
|
||||
21 | ## invalid
|
||||
|
|
||||
= help: Replace with f-string
|
||||
|
||||
FURB116.py:32:7: FURB116 Replace `bin` call with `f"{d:b}"`
|
||||
|
|
||||
30 | d = datetime.datetime.now(tz=datetime.UTC)
|
||||
31 | # autofix is display-only
|
||||
32 | print(bin(d)[2:])
|
||||
| ^^^^^^^^^^ FURB116
|
||||
33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
34 | print(bin(len("xyz").numerator)[2:])
|
||||
|
|
||||
= help: Replace with `f"{d:b}"`
|
||||
|
||||
ℹ Display-only fix
|
||||
29 29 |
|
||||
30 30 | d = datetime.datetime.now(tz=datetime.UTC)
|
||||
31 31 | # autofix is display-only
|
||||
32 |-print(bin(d)[2:])
|
||||
32 |+print(f"{d:b}")
|
||||
33 33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
34 34 | print(bin(len("xyz").numerator)[2:])
|
||||
35 35 |
|
||||
|
||||
FURB116.py:34:7: FURB116 Replace `bin` call with `f"{len("xyz").numerator:b}"`
|
||||
|
|
||||
32 | print(bin(d)[2:])
|
||||
33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
34 | print(bin(len("xyz").numerator)[2:])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
|
||||
35 |
|
||||
36 | # autofix is display-only
|
||||
|
|
||||
= help: Replace with `f"{len("xyz").numerator:b}"`
|
||||
|
||||
ℹ Display-only fix
|
||||
31 31 | # autofix is display-only
|
||||
32 32 | print(bin(d)[2:])
|
||||
33 33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
34 |-print(bin(len("xyz").numerator)[2:])
|
||||
34 |+print(f"{len("xyz").numerator:b}")
|
||||
35 35 |
|
||||
36 36 | # autofix is display-only
|
||||
37 37 | print(bin({0: 1}[0].numerator)[2:])
|
||||
|
||||
FURB116.py:37:7: FURB116 Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"`
|
||||
|
|
||||
36 | # autofix is display-only
|
||||
37 | print(bin({0: 1}[0].numerator)[2:])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
|
||||
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
39 | print(bin(ord("\\").numerator)[2:])
|
||||
|
|
||||
= help: Replace with `f"{ {0: 1}[0].numerator:b}"`
|
||||
|
||||
ℹ Display-only fix
|
||||
34 34 | print(bin(len("xyz").numerator)[2:])
|
||||
35 35 |
|
||||
36 36 | # autofix is display-only
|
||||
37 |-print(bin({0: 1}[0].numerator)[2:])
|
||||
37 |+print(f"{ {0: 1}[0].numerator:b}")
|
||||
38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
39 39 | print(bin(ord("\\").numerator)[2:])
|
||||
40 40 | print(hex(sys
|
||||
|
||||
FURB116.py:39:7: FURB116 Replace `bin` call with `f"{ord("\\").numerator:b}"`
|
||||
|
|
||||
37 | print(bin({0: 1}[0].numerator)[2:])
|
||||
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
39 | print(bin(ord("\\").numerator)[2:])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116
|
||||
40 | print(hex(sys
|
||||
41 | .maxunicode)[2:])
|
||||
|
|
||||
= help: Replace with `f"{ord("\\").numerator:b}"`
|
||||
|
||||
ℹ Display-only fix
|
||||
36 36 | # autofix is display-only
|
||||
37 37 | print(bin({0: 1}[0].numerator)[2:])
|
||||
38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
39 |-print(bin(ord("\\").numerator)[2:])
|
||||
39 |+print(f"{ord("\\").numerator:b}")
|
||||
40 40 | print(hex(sys
|
||||
41 41 | .maxunicode)[2:])
|
||||
42 42 |
|
||||
|
||||
FURB116.py:40:7: FURB116 Replace `hex` call with f-string
|
||||
|
|
||||
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
39 | print(bin(ord("\\").numerator)[2:])
|
||||
40 | print(hex(sys
|
||||
| _______^
|
||||
41 | | .maxunicode)[2:])
|
||||
| |________________^ FURB116
|
||||
42 |
|
||||
43 | # for negatives numbers autofix is display-only
|
||||
|
|
||||
= help: Replace with f-string
|
||||
|
||||
ℹ Display-only fix
|
||||
37 37 | print(bin({0: 1}[0].numerator)[2:])
|
||||
38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
|
||||
39 39 | print(bin(ord("\\").numerator)[2:])
|
||||
40 |-print(hex(sys
|
||||
41 |-.maxunicode)[2:])
|
||||
40 |+print(f"{sys
|
||||
41 |+.maxunicode:x}")
|
||||
42 42 |
|
||||
43 43 | # for negatives numbers autofix is display-only
|
||||
44 44 | print(bin(-1)[2:])
|
||||
|
||||
FURB116.py:44:7: FURB116 Replace `bin` call with `f"{-1:b}"`
|
||||
|
|
||||
43 | # for negatives numbers autofix is display-only
|
||||
44 | print(bin(-1)[2:])
|
||||
| ^^^^^^^^^^^ FURB116
|
||||
|
|
||||
= help: Replace with `f"{-1:b}"`
|
||||
|
||||
ℹ Display-only fix
|
||||
41 41 | .maxunicode)[2:])
|
||||
42 42 |
|
||||
43 43 | # for negatives numbers autofix is display-only
|
||||
44 |-print(bin(-1)[2:])
|
||||
44 |+print(f"{-1:b}")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user