Compare commits

..

2 Commits

Author SHA1 Message Date
Micha Reiser
f78c03c5dc Move more projects to good.txt 2025-05-08 19:34:56 +02:00
Micha Reiser
1fa5e1fbbc Update salsa 2025-05-08 19:28:35 +02:00
373 changed files with 4520 additions and 12656 deletions

View File

@@ -1,9 +1,8 @@
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following:
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull
requests.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

View File

@@ -40,7 +40,7 @@ jobs:
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
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.16.0
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
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.4.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
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.4.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
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.16.0
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
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.4.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -239,11 +239,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -297,11 +297,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -324,7 +324,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -407,11 +407,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # 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@13f9d60d5358393bf14644dba56d9f123bc5d595 # v1.12.4
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
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@13f9d60d5358393bf14644dba56d9f123bc5d595 # v1.12.4
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
- 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@83254c543806f3224380bf1001d6fac8feaf2d0b # v2
uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2
with:
tool: cargo-codspeed

View File

@@ -69,7 +69,7 @@ jobs:
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@968b2b61c05f84462d6fcc78d2f5205bbb8b98c2" \
--from="git+https://github.com/hauntsaninja/mypy_primer@4b15cf3b07db69db67bbfaebfffb2a8a28040933" \
mypy_primer \
--repo ruff \
--type-checker ty \

View File

@@ -5,7 +5,7 @@ exclude: |
.github/workflows/release.yml|
crates/ty_vendored/vendor/.*|
crates/ty_project/resources/.*|
crates/ty/docs/(configuration|rules|cli).md|
crates/ty/docs/rules.md|
crates/ruff_benchmark/resources/.*|
crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*|
@@ -80,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.9
rev: v0.11.8
hooks:
- id: ruff-format
- id: ruff
@@ -98,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.7.0
rev: v1.6.0
hooks:
- id: zizmor

View File

@@ -1,37 +1,5 @@
# 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

View File

@@ -2,11 +2,6 @@
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
> [!NOTE]
>
> This guide is for Ruff. If you're looking to contribute to ty, please see [the ty contributing
> guide](https://github.com/astral-sh/ruff/blob/main/crates/ty/CONTRIBUTING.md).
## The Basics
Ruff welcomes contributions in the form of pull requests.

104
Cargo.lock generated
View File

@@ -334,9 +334,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.38"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
"clap_derive",
@@ -344,9 +344,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.38"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
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 0.29.0",
"nix",
"terminfo",
"thiserror 2.0.12",
"which",
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -487,7 +487,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -681,11 +681,11 @@ dependencies = [
[[package]]
name = "ctrlc"
version = "3.4.7"
version = "3.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
dependencies = [
"nix 0.30.1",
"nix",
"windows-sys 0.59.0",
]
@@ -1057,9 +1057,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.3.3"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"js-sys",
@@ -1539,9 +1539,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.13"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
@@ -1554,9 +1554,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.13"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300"
dependencies = [
"proc-macro2",
"quote",
@@ -1771,15 +1771,6 @@ 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"
@@ -1880,18 +1871,6 @@ 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"
@@ -2470,7 +2449,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
"getrandom 0.3.2",
]
[[package]]
@@ -2570,7 +2549,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.9"
version = "0.11.8"
dependencies = [
"anyhow",
"argfile",
@@ -2604,7 +2583,6 @@ dependencies = [
"ruff_linter",
"ruff_macros",
"ruff_notebook",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_parser",
@@ -2723,7 +2701,6 @@ dependencies = [
"indoc",
"itertools 0.14.0",
"libcst",
"markdown",
"pretty_assertions",
"rayon",
"regex",
@@ -2732,7 +2709,6 @@ dependencies = [
"ruff_formatter",
"ruff_linter",
"ruff_notebook",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_codegen",
"ruff_python_formatter",
@@ -2749,7 +2725,6 @@ dependencies = [
"tracing",
"tracing-indicatif",
"tracing-subscriber",
"ty",
"ty_project",
"url",
]
@@ -2811,7 +2786,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.9"
version = "0.11.8"
dependencies = [
"aho-corasick",
"anyhow",
@@ -2901,13 +2876,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "ruff_options_metadata"
version = "0.0.0"
dependencies = [
"serde",
]
[[package]]
name = "ruff_python_ast"
version = "0.0.0"
@@ -3146,11 +3114,11 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.9"
version = "0.11.8"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.3",
"getrandom 0.3.2",
"js-sys",
"log",
"ruff_formatter",
@@ -3194,7 +3162,6 @@ dependencies = [
"ruff_graph",
"ruff_linter",
"ruff_macros",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_semantic",
@@ -3231,12 +3198,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc-stable-hash"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08"
[[package]]
name = "rustix"
version = "0.38.44"
@@ -3278,7 +3239,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
source = "git+https://github.com/salsa-rs/salsa.git?rev=af69cc11146352ec2eb6d972537e99c473ac3748#af69cc11146352ec2eb6d972537e99c473ac3748"
dependencies = [
"boxcar",
"compact_str",
@@ -3301,12 +3262,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
source = "git+https://github.com/salsa-rs/salsa.git?rev=af69cc11146352ec2eb6d972537e99c473ac3748#af69cc11146352ec2eb6d972537e99c473ac3748"
[[package]]
name = "salsa-macros"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
source = "git+https://github.com/salsa-rs/salsa.git?rev=af69cc11146352ec2eb6d972537e99c473ac3748#af69cc11146352ec2eb6d972537e99c473ac3748"
dependencies = [
"heck",
"proc-macro2",
@@ -3643,7 +3604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.2",
"windows-sys 0.59.0",
@@ -4002,7 +3963,6 @@ dependencies = [
"crossbeam",
"ctrlc",
"filetime",
"indicatif",
"insta",
"insta-cmd",
"jiff",
@@ -4055,7 +4015,6 @@ dependencies = [
"ruff_cache",
"ruff_db",
"ruff_macros",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_text_size",
@@ -4159,7 +4118,6 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-stable-hash",
"salsa",
"serde",
"smallvec",
@@ -4187,7 +4145,7 @@ version = "0.0.0"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.3",
"getrandom 0.3.2",
"js-sys",
"log",
"ruff_db",
@@ -4263,12 +4221,6 @@ 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"
@@ -4372,7 +4324,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.3",
"getrandom 0.3.2",
"js-sys",
"rand 0.9.1",
"uuid-macro-internal",
@@ -4645,7 +4597,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -23,7 +23,6 @@ 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" }
@@ -38,7 +37,6 @@ 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" }
@@ -125,9 +123,8 @@ rand = { version = "0.9.0" }
rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "7edce6e248f35c8114b4b021cdb474a3fb2813b3" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "af69cc11146352ec2eb6d972537e99c473ac3748" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -185,7 +182,7 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom", "ruff_options_metadata"]
ignored = ["getrandom"]
[workspace.lints.rust]

View File

@@ -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.9/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.9/install.ps1 | iex"
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"
```
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.9
rev: v0.11.8
hooks:
# Run the linter.
- id: ruff
@@ -255,7 +255,7 @@ indent-width = 4
target-version = "py39"
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"]
ignore = []

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.11.9"
version = "0.11.8"
publish = true
authors = { workspace = true }
edition = { workspace = true }
@@ -20,7 +20,6 @@ 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 }
@@ -84,7 +83,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"), not(target_os = "android"), 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"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
tikv-jemallocator = { workspace = true }
[lints]

View File

@@ -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;

View File

@@ -2,8 +2,10 @@ use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
use itertools::Itertools;
use std::str::FromStr;
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
use ruff_workspace::options::Options;
use ruff_workspace::{
options::Options,
options_base::{OptionField, OptionSet, OptionsMetadata, Visit},
};
#[derive(Default)]
struct CollectOptionsVisitor {

View File

@@ -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<()> {

View File

@@ -15,7 +15,6 @@ 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",

View File

@@ -301,62 +301,6 @@ fn benchmark_many_string_assignments(criterion: &mut Criterion) {
});
}
fn benchmark_many_tuple_assignments(criterion: &mut Criterion) {
setup_rayon();
criterion.bench_function("ty_micro[many_tuple_assignments]", |b| {
b.iter_batched_ref(
|| {
// This is a micro benchmark, but it is effectively identical to a code sample
// observed in https://github.com/astral-sh/ty/issues/362
setup_micro_case(
r#"
def flag() -> bool:
return True
t = ()
if flag():
t += (1,)
if flag():
t += (2,)
if flag():
t += (3,)
if flag():
t += (4,)
if flag():
t += (5,)
if flag():
t += (6,)
if flag():
t += (7,)
if flag():
t += (8,)
if flag():
t += (9,)
if flag():
t += (10,)
if flag():
t += (11,)
# Perform some kind of operation on the union type
print(1 in t)
"#,
)
},
|case| {
let Case { db, .. } = case;
let result = db.check().unwrap();
assert_eq!(result.len(), 0);
},
BatchSize::SmallInput,
);
});
}
criterion_group!(check_file, benchmark_cold, benchmark_incremental);
criterion_group!(
micro,
benchmark_many_string_assignments,
benchmark_many_tuple_assignments
);
criterion_group!(micro, benchmark_many_string_assignments);
criterion_main!(check_file, micro);

View File

@@ -2,6 +2,7 @@ use std::{fmt::Formatter, sync::Arc};
use render::{FileResolver, Input};
use ruff_source_file::{SourceCode, SourceFile};
use thiserror::Error;
use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange};
@@ -612,19 +613,40 @@ impl DiagnosticId {
code.split_once(':').map(|(_, rest)| rest)
}
/// Returns a concise description of this diagnostic ID.
/// Returns `true` if this `DiagnosticId` matches the given name.
///
/// 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 {
/// ## 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 {
DiagnosticId::Panic => "panic",
DiagnosticId::Io => "io",
DiagnosticId::InvalidSyntax => "invalid-syntax",
DiagnosticId::Lint(name) => name.as_str(),
DiagnosticId::Lint(name) => {
return Err(DiagnosticAsStrError::Category {
category: "lint",
name: name.as_str(),
})
}
DiagnosticId::RevealedType => "revealed-type",
DiagnosticId::UnknownRule => "unknown-rule",
}
})
}
pub fn is_invalid_syntax(&self) -> bool {
@@ -632,9 +654,26 @@ impl DiagnosticId {
}
}
#[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,
},
}
impl std::fmt::Display for DiagnosticId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
match self.as_str() {
Ok(name) => f.write_str(name),
Err(DiagnosticAsStrError::Category { category, name }) => {
write!(f, "{category}:{name}")
}
}
}
}

View File

@@ -140,6 +140,7 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
/// both.)
#[derive(Debug)]
struct Resolved<'a> {
id: String,
diagnostics: Vec<ResolvedDiagnostic<'a>>,
}
@@ -151,12 +152,14 @@ impl<'a> Resolved<'a> {
for sub in &diag.inner.subs {
diagnostics.push(ResolvedDiagnostic::from_sub_diagnostic(resolver, sub));
}
Resolved { diagnostics }
let id = diag.inner.id.to_string();
Resolved { id, 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()
@@ -174,7 +177,6 @@ impl<'a> Resolved<'a> {
#[derive(Debug)]
struct ResolvedDiagnostic<'a> {
severity: Severity,
id: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
}
@@ -195,11 +197,20 @@ impl<'a> ResolvedDiagnostic<'a> {
ResolvedAnnotation::new(path, &diagnostic_source, ann)
})
.collect();
let id = Some(diag.inner.id.to_string());
let message = diag.inner.message.as_str().to_string();
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(),
)
};
ResolvedDiagnostic {
severity: diag.inner.severity,
id,
message,
annotations,
}
@@ -222,7 +233,6 @@ impl<'a> ResolvedDiagnostic<'a> {
.collect();
ResolvedDiagnostic {
severity: diag.inner.severity,
id: None,
message: diag.inner.message.as_str().to_string(),
annotations,
}
@@ -289,7 +299,6 @@ 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,
}
@@ -369,6 +378,20 @@ 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>>,
}
@@ -377,12 +400,6 @@ 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,
@@ -403,11 +420,7 @@ impl RenderableDiagnostic<'_> {
.iter()
.map(|snippet| snippet.to_annotate(path))
});
let mut message = level.title(self.message);
if let Some(id) = self.id {
message = message.id(id);
}
message.snippets(snippets)
level.title(self.message).snippets(snippets)
}
}
@@ -786,7 +799,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -810,7 +823,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
warning[test-diagnostic]: main diagnostic message
warning: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -830,7 +843,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
info[test-diagnostic]: main diagnostic message
info: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -857,7 +870,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -876,7 +889,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -897,7 +910,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> non-ascii:5:1
|
3 | ΔΔΔΔΔΔΔΔΔΔΔΔ
@@ -916,7 +929,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> non-ascii:2:2
|
1 | ☃☃☃☃☃☃☃☃☃☃☃☃
@@ -940,7 +953,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
4 | dog
@@ -957,7 +970,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
5 | elephant
@@ -972,7 +985,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -989,7 +1002,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:11:1
|
9 | inchworm
@@ -1006,7 +1019,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
1 | aardvark
@@ -1039,7 +1052,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1083,7 +1096,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1108,7 +1121,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1136,7 +1149,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1164,7 +1177,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1189,7 +1202,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1220,7 +1233,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:1:1
|
1 | aardvark
@@ -1258,7 +1271,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> spacey-animals:8:1
|
7 | dog
@@ -1275,7 +1288,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> spacey-animals:12:1
|
11 | gorilla
@@ -1293,7 +1306,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> spacey-animals:13:1
|
11 | gorilla
@@ -1333,7 +1346,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> spacey-animals:3:1
|
3 | beetle
@@ -1362,7 +1375,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1399,7 +1412,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1436,7 +1449,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1464,7 +1477,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1500,7 +1513,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1539,7 +1552,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1587,7 +1600,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:3:1
|
1 | aardvark
@@ -1623,7 +1636,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1646,7 +1659,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1666,7 +1679,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1686,7 +1699,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:4
|
3 | canary
@@ -1708,7 +1721,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:4
|
3 | canary
@@ -1740,7 +1753,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:4:1
|
2 | beetle
@@ -1769,7 +1782,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:4:1
|
2 | beetle
@@ -1800,7 +1813,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1835,7 +1848,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1863,7 +1876,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
3 | canary
@@ -1895,7 +1908,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:3
|
3 | canary
@@ -1917,7 +1930,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:3
|
3 | canary
@@ -1950,7 +1963,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:8:1
|
6 | finch
@@ -1990,7 +2003,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:5:1
|
5 | elephant
@@ -2034,7 +2047,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> fruits:1:1
|
1 | apple
@@ -2069,7 +2082,7 @@ watermelon
insta::assert_snapshot!(
env.render(&diag),
@r"
error[test-diagnostic]: main diagnostic message
error: lint:test-diagnostic: main diagnostic message
--> animals:11:1
|
11 | kangaroo

View File

@@ -277,7 +277,7 @@ impl std::panic::RefUnwindSafe for Files {}
#[salsa::input]
pub struct File {
/// The path of the file (immutable).
#[returns(ref)]
#[return_ref]
pub path: FilePath,
/// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows

View File

@@ -19,8 +19,8 @@ use crate::Db;
#[salsa::input(debug)]
pub struct FileRoot {
/// The path of a root is guaranteed to never change.
#[returns(deref)]
pub path: SystemPathBuf,
#[return_ref]
path_buf: SystemPathBuf,
/// The kind of the root at the time of its creation.
kind_at_time_of_creation: FileRootKind,
@@ -32,6 +32,10 @@ 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()
}

View File

@@ -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(returns(ref), no_eq)]
#[salsa::tracked(return_ref, no_eq)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let _span = tracing::trace_span!("parsed_module", ?file).entered();

View File

@@ -11,14 +11,12 @@ 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 }
@@ -33,7 +31,6 @@ 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 }

View File

@@ -3,8 +3,7 @@
use anyhow::Result;
use crate::{
generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference,
generate_ty_options, generate_ty_rules, generate_ty_schema,
generate_cli_help, generate_docs, generate_json_schema, generate_ty_rules, generate_ty_schema,
};
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
@@ -41,8 +40,6 @@ 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(())
}

View File

@@ -1,4 +1,5 @@
//! Generate CLI help.
#![allow(clippy::print_stdout)]
use std::path::PathBuf;
use std::{fs, str};

View File

@@ -1,4 +1,5 @@
//! Generate Markdown documentation for applicable rules.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::collections::HashSet;
use std::fmt::Write as _;
@@ -12,8 +13,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;

View File

@@ -1,3 +1,5 @@
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

View File

@@ -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();

View File

@@ -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 = "🧪";

View File

@@ -1,334 +0,0 @@
//! 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(&current, &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 })
}
}

View File

@@ -1,257 +0,0 @@
//! 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(&current, &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(())
}
}

View File

@@ -1,4 +1,5 @@
//! Generates the rules table for ty
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::borrow::Cow;
use std::fmt::Write as _;

View File

@@ -1,3 +1,5 @@
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

View File

@@ -2,8 +2,6 @@
//!
//! 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};
@@ -17,8 +15,6 @@ 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;
@@ -52,7 +48,6 @@ enum Command {
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.
@@ -97,7 +92,6 @@ fn main() -> Result<ExitCode> {
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)?,

View File

@@ -1,4 +1,5 @@
//! Print the AST for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::path::PathBuf;

View File

@@ -1,4 +1,5 @@
//! Print the `LibCST` CST for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

View File

@@ -1,4 +1,5 @@
//! Print the token stream for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::path::PathBuf;

View File

@@ -1,4 +1,5 @@
//! 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;

View File

@@ -92,7 +92,7 @@ impl std::fmt::Display for IndentStyle {
}
}
/// The visual width of an indentation.
/// The visual width of a 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 these format options
/// Derives the print options from the these format options
fn as_print_options(&self) -> PrinterOptions;
}

View File

@@ -88,8 +88,8 @@ impl Db for ModuleDb {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
fn rule_selection(&self) -> Arc<RuleSelection> {
self.rule_selection.clone()
}
fn lint_registry(&self) -> &LintRegistry {

View File

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

View File

@@ -24,6 +24,9 @@ from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.datasets import DatasetAliasEvent
from airflow.hooks.base_hook import BaseHook
from airflow.operators.subdag import SubDagOperator
from airflow.providers.mysql.datasets import mysql
from airflow.providers.postgres.datasets import postgres
from airflow.providers.trino.datasets import trino
from airflow.secrets.local_filesystem import LocalFilesystemBackend
from airflow.sensors.base_sensor_operator import BaseSensorOperator
from airflow.triggers.external_task import TaskStateTrigger
@@ -75,6 +78,14 @@ BaseHook()
# airflow.operators.subdag.*
SubDagOperator()
# airflow.providers.mysql
mysql.sanitize_uri
# airflow.providers.postgres
postgres.sanitize_uri
# airflow.providers.trino
trino.sanitize_uri
# airflow.secrets
# get_connection
@@ -144,18 +155,3 @@ should_hide_value_for_key
from airflow.operators.python import get_current_context
get_current_context()
# airflow.providers.mysql
from airflow.providers.mysql.datasets.mysql import sanitize_uri
sanitize_uri
# airflow.providers.postgres
from airflow.providers.postgres.datasets.postgres import sanitize_uri
sanitize_uri
# airflow.providers.trino
from airflow.providers.trino.datasets.trino import sanitize_uri
sanitize_uri

View File

@@ -1,8 +1,17 @@
from __future__ import annotations
try:
from airflow.assets.manager import AssetManager
from airflow.sdk import Asset
except ModuleNotFoundError:
from airflow.datasets.manager import DatasetManager as AssetManager
from airflow.datasets import Dataset as Asset
AssetManager()
Asset
try:
from airflow.sdk import Asset
except ModuleNotFoundError:
from airflow import datasets
Asset = datasets.Dataset
asset = Asset()

View File

@@ -1,8 +0,0 @@
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()

View File

@@ -13,10 +13,6 @@ 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
@@ -46,9 +42,7 @@ ObjectStoragePath()
attach()
# airflow.models
Connection()
DAGFromModel()
Variable()
# airflow.models.baseoperator
chain()

View File

@@ -1,8 +0,0 @@
from __future__ import annotations
try:
from airflow.sdk import Asset
except ModuleNotFoundError:
from airflow.datasets import Dataset as Asset
Asset()

View File

@@ -1,8 +0,0 @@
from __future__ import annotations
try:
from airflow.providers.standard.triggers.file import FileTrigger
except ModuleNotFoundError:
from airflow.triggers.file import FileTrigger
FileTrigger()

View File

@@ -166,6 +166,3 @@ query60 = f"""
foo
FROM ({user_input}) raw
"""
# https://github.com/astral-sh/ruff/issues/17967
query61 = f"SELECT * FROM table" # skip expressionless f-strings

View File

@@ -25,11 +25,3 @@ 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)

View File

@@ -28,14 +28,3 @@ 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,
}
)

View File

@@ -12,38 +12,3 @@ def test_xxx(_fixture): # Error arg
def test_xxx(*, _fixture): # Error kwonly
pass
# https://github.com/astral-sh/ruff/issues/17599
@pytest.mark.parametrize("_foo", [1, 2, 3])
def test_thingy(_foo): # Ok defined in parametrize
pass
@pytest.mark.parametrize(
"_test_input,_expected",
[("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(_test_input, _expected): # OK defined in parametrize
pass
@pytest.mark.parametrize("_foo", [1, 2, 3])
def test_thingy2(_foo, _bar): # Error _bar is not defined in parametrize
pass
@pytest.mark.parametrize(["_foo", "_bar"], [1, 2, 3])
def test_thingy3(_foo, _bar): # OK defined in parametrize
pass
@pytest.mark.parametrize(("_foo"), [1, 2, 3])
def test_thingy4(_foo, _bar): # Error _bar is not defined in parametrize
pass
@pytest.mark.parametrize([" _foo", " _bar "], [1, 2, 3])
def test_thingy5(_foo, _bar): # OK defined in parametrize
pass
x = "other"
@pytest.mark.parametrize(x, [1, 2, 3])
def test_thingy5(_foo, _bar): # known false negative, we don't try to resolve variables
pass

View File

@@ -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,27 +106,3 @@ 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))
# https://github.com/astral-sh/ruff/issues/18069
print("".split(maxsplit=0))
print("".split(sep=None, maxsplit=0))
print(" ".split(maxsplit=0))
print(" ".split(sep=None, maxsplit=0))
print(" x ".split(maxsplit=0))
print(" x ".split(sep=None, maxsplit=0))
print(" x ".split(maxsplit=0))
print(" x ".split(sep=None, maxsplit=0))
print("".rsplit(maxsplit=0))
print("".rsplit(sep=None, maxsplit=0))
print(" ".rsplit(maxsplit=0))
print(" ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))

View File

@@ -9,7 +9,3 @@ 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))

View File

@@ -87,20 +87,3 @@ 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)

View File

@@ -144,14 +144,14 @@ def f():
def f():
# make sure that `tmp` is not deleted
tmp = 1; result = [] # comment should be protected
tmp = 1; result = [] # commment 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 # comment should be protected
result = []; tmp = 1 # commment should be protected
for i in range(10):
result.append(i + 1) # PERF401

View File

@@ -6,16 +6,14 @@ from .mmap import error
raise error
# Testing the modules
import socket, mmap, select, resource
import socket, mmap, select
raise socket.error
raise mmap.error
raise select.error
raise resource.error
raise socket.error()
raise mmap.error(1)
raise select.error(1, 2)
raise resource.error(1, "strerror", "filename")
raise socket.error(
1,
@@ -32,9 +30,6 @@ raise error(1)
from select import error
raise error(1, 2)
from resource import error
raise error(1, "strerror", "filename")
# Testing the names
raise EnvironmentError
raise IOError

View File

@@ -1,6 +1,3 @@
import datetime
import sys
num = 1337
def return_num() -> int:
@@ -13,7 +10,6 @@ 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)
@@ -26,19 +22,3 @@ 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:])

View File

@@ -19,9 +19,6 @@ 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]
@@ -38,7 +35,3 @@ 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()}

View File

@@ -1116,95 +1116,6 @@ 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,

View File

@@ -8,20 +8,13 @@ use ruff_python_semantic::SemanticModel;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Replacement {
// There's no replacement or suggestion other than removal
None,
// The attribute name of a class has been changed.
AttrName(&'static str),
// Additional information. Used when there's replacement but they're not direct mapping.
Name(&'static str),
Message(&'static str),
// Symbols updated in Airflow 3 with replacement
// e.g., `airflow.datasets.Dataset` to `airflow.sdk.Asset`
AutoImport {
module: &'static str,
name: &'static str,
},
// Symbols updated in Airflow 3 with only module changed. Used when we want to match multiple names.
// e.g., `airflow.configuration.as_dict | get` to `airflow.configuration.conf.as_dict | get`
SourceModuleMoved {
module: &'static str,
name: String,
@@ -52,8 +45,7 @@ pub(crate) enum ProviderReplacement {
pub(crate) fn is_guarded_by_try_except(
expr: &Expr,
module: &str,
name: &str,
replacement: &Replacement,
semantic: &SemanticModel,
) -> bool {
match expr {
@@ -71,7 +63,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, module, name, semantic)
try_block_contains_undeprecated_attribute(try_node, replacement, semantic)
}
Expr::Name(ExprName { id, .. }) => {
let Some(binding_id) = semantic.lookup_symbol(id.as_str()) else {
@@ -97,7 +89,7 @@ pub(crate) fn is_guarded_by_try_except(
{
return false;
}
try_block_contains_undeprecated_import(try_node, module, name)
try_block_contains_undeprecated_import(try_node, replacement)
}
_ => false,
}
@@ -108,10 +100,12 @@ 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,
module: &str,
name: &str,
replacement: &Replacement,
semantic: &SemanticModel,
) -> bool {
let Replacement::AutoImport { module, name } = replacement else {
return false;
};
let undeprecated_qualified_name = {
let mut builder = QualifiedNameBuilder::default();
for part in module.split('.') {
@@ -128,7 +122,10 @@ 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, module: &str, name: &str) -> bool {
fn try_block_contains_undeprecated_import(try_node: &StmtTry, replacement: &Replacement) -> bool {
let Replacement::AutoImport { module, name } = replacement else {
return false;
};
let mut import_searcher = ImportSearcher::new(module, name);
import_searcher.visit_body(&try_node.body);
import_searcher.found_import

View File

@@ -46,12 +46,9 @@ 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(

View File

@@ -1,5 +1,5 @@
use crate::importer::ImportRequest;
use crate::rules::airflow::helpers::{is_guarded_by_try_except, ProviderReplacement};
use crate::rules::airflow::helpers::ProviderReplacement;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{Expr, ExprAttribute};
@@ -937,17 +937,13 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
ranged.range(),
);
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;
}
if let ProviderReplacement::AutoImport {
module,
name,
provider: _,
version: _,
} = replacement
{
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, name),
@@ -958,5 +954,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
Ok(Fix::safe_edits(import_edit, [replacement_edit]))
});
}
checker.report_diagnostic(diagnostic);
}

View File

@@ -57,27 +57,28 @@ impl Violation for Airflow3Removal {
} = self;
match replacement {
Replacement::None
| Replacement::AttrName(_)
| Replacement::Message(_)
| Replacement::Name(_)
| Replacement::AutoImport { module: _, name: _ }
| Replacement::SourceModuleMoved { module: _, name: _ } => {
format!("`{deprecated}` is removed in Airflow 3.0")
}
Replacement::Message(message) => {
format!("`{deprecated}` is removed in Airflow 3.0; {message}")
}
}
}
fn fix_title(&self) -> Option<String> {
let Airflow3Removal { replacement, .. } = self;
match replacement {
Replacement::None => None,
Replacement::AttrName(name) => Some(format!("Use `{name}` instead")),
Replacement::Message(message) => Some((*message).to_string()),
Replacement::Name(name) => Some(format!("Use `{name}` instead")),
Replacement::AutoImport { module, name } => {
Some(format!("Use `{module}.{name}` instead"))
}
Replacement::SourceModuleMoved { module, name } => {
Some(format!("Use `{module}.{name}` instead"))
}
_ => None,
}
}
}
@@ -277,22 +278,22 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) {
let replacement = match *qualname.segments() {
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
"dataset_factories" => Replacement::AttrName("asset_factories"),
"dataset_uri_handlers" => Replacement::AttrName("asset_uri_handlers"),
"dataset_factories" => Replacement::Name("asset_factories"),
"dataset_uri_handlers" => Replacement::Name("asset_uri_handlers"),
"dataset_to_openlineage_converters" => {
Replacement::AttrName("asset_to_openlineage_converters")
Replacement::Name("asset_to_openlineage_converters")
}
_ => return,
},
["airflow", "lineage", "hook", "DatasetLineageInfo"] => match attr.as_str() {
"dataset" => Replacement::AttrName("asset"),
"dataset" => Replacement::Name("asset"),
_ => return,
},
_ => return,
};
// Create the `Fix` first to avoid cloning `Replacement`.
let fix = if let Replacement::AttrName(name) = replacement {
let fix = if let Replacement::Name(name) = replacement {
Some(Fix::safe_edit(Edit::range_replacement(
name.to_string(),
attr.range(),
@@ -465,52 +466,52 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
let replacement = match qualname.segments() {
["airflow", "datasets", "manager", "DatasetManager"] => match attr.as_str() {
"register_dataset_change" => Replacement::AttrName("register_asset_change"),
"create_datasets" => Replacement::AttrName("create_assets"),
"notify_dataset_created" => Replacement::AttrName("notify_asset_created"),
"notify_dataset_changed" => Replacement::AttrName("notify_asset_changed"),
"notify_dataset_alias_created" => Replacement::AttrName("notify_asset_alias_created"),
"register_dataset_change" => Replacement::Name("register_asset_change"),
"create_datasets" => Replacement::Name("create_assets"),
"notify_dataset_created" => Replacement::Name("notify_asset_created"),
"notify_dataset_changed" => Replacement::Name("notify_asset_changed"),
"notify_dataset_alias_created" => Replacement::Name("notify_asset_alias_created"),
_ => return,
},
["airflow", "lineage", "hook", "HookLineageCollector"] => match attr.as_str() {
"create_dataset" => Replacement::AttrName("create_asset"),
"add_input_dataset" => Replacement::AttrName("add_input_asset"),
"add_output_dataset" => Replacement::AttrName("add_output_asset"),
"collected_datasets" => Replacement::AttrName("collected_assets"),
"create_dataset" => Replacement::Name("create_asset"),
"add_input_dataset" => Replacement::Name("add_input_asset"),
"add_output_dataset" => Replacement::Name("add_output_asset"),
"collected_datasets" => Replacement::Name("collected_assets"),
_ => return,
},
["airflow", "providers", "amazon", "auth_manager", "aws_auth_manager", "AwsAuthManager"] => {
match attr.as_str() {
"is_authorized_dataset" => Replacement::AttrName("is_authorized_asset"),
"is_authorized_dataset" => Replacement::Name("is_authorized_asset"),
_ => return,
}
}
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
"initialize_providers_dataset_uri_resources" => {
Replacement::AttrName("initialize_providers_asset_uri_resources")
Replacement::Name("initialize_providers_asset_uri_resources")
}
_ => return,
},
["airflow", "secrets", "local_filesystem", "LocalFilesystemBackend"] => match attr.as_str()
{
"get_connections" => Replacement::AttrName("get_connection"),
"get_connections" => Replacement::Name("get_connection"),
_ => return,
},
["airflow", "datasets", ..] | ["airflow", "Dataset"] => match attr.as_str() {
"iter_datasets" => Replacement::AttrName("iter_assets"),
"iter_dataset_aliases" => Replacement::AttrName("iter_asset_aliases"),
"iter_datasets" => Replacement::Name("iter_assets"),
"iter_dataset_aliases" => Replacement::Name("iter_asset_aliases"),
_ => return,
},
segments => {
if is_airflow_secret_backend(segments) {
match attr.as_str() {
"get_conn_uri" => Replacement::AttrName("get_conn_value"),
"get_connections" => Replacement::AttrName("get_connection"),
"get_conn_uri" => Replacement::Name("get_conn_value"),
"get_connections" => Replacement::Name("get_connection"),
_ => return,
}
} else if is_airflow_hook(segments) {
match attr.as_str() {
"get_connections" => Replacement::AttrName("get_connection"),
"get_connections" => Replacement::Name("get_connection"),
_ => return,
}
} else {
@@ -519,7 +520,7 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
}
};
// Create the `Fix` first to avoid cloning `Replacement`.
let fix = if let Replacement::AttrName(name) = replacement {
let fix = if let Replacement::Name(name) = replacement {
Some(Fix::safe_edit(Edit::range_replacement(
name.to_string(),
attr.range(),
@@ -565,12 +566,12 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
let replacement = match qualified_name.segments() {
// airflow.PY\d{1,2}
["airflow", "PY36" | "PY37" | "PY38" | "PY39" | "PY310" | "PY311" | "PY312"] => {
Replacement::Message("Use `sys.version_info` instead")
Replacement::Name("sys.version_info")
}
// airflow.api_connexion.security
["airflow", "api_connexion", "security", "requires_access"] => {
Replacement::Message("Use `airflow.api_connexion.security.requires_access_*` instead")
Replacement::Name("airflow.api_connexion.security.requires_access_*")
}
["airflow", "api_connexion", "security", "requires_access_dataset"] => {
Replacement::AutoImport {
@@ -625,10 +626,9 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
["airflow", "datasets", "DatasetAliasEvent"] => Replacement::None,
// airflow.hooks
["airflow", "hooks", "base_hook", "BaseHook"] => Replacement::AutoImport {
module: "airflow.hooks.base",
name: "BaseHook",
},
["airflow", "hooks", "base_hook", "BaseHook"] => {
Replacement::Name("airflow.hooks.base.BaseHook")
}
// airflow.lineage.hook
["airflow", "lineage", "hook", "DatasetLineageInfo"] => Replacement::AutoImport {
@@ -664,10 +664,9 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
},
// airflow.notifications
["airflow", "notifications", "basenotifier", "BaseNotifier"] => Replacement::AutoImport {
module: "airflow.sdk",
name: "BaseNotifier",
},
["airflow", "notifications", "basenotifier", "BaseNotifier"] => {
Replacement::Name("airflow.sdk.BaseNotifier")
}
// airflow.operators
["airflow", "operators", "subdag", ..] => {
@@ -692,10 +691,7 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
// airflow.sensors
["airflow", "sensors", "base_sensor_operator", "BaseSensorOperator"] => {
Replacement::AutoImport {
module: "airflow.sdk.bases.sensor",
name: "BaseSensorOperator",
}
Replacement::Name("airflow.sdk.bases.sensor.BaseSensorOperator")
}
// airflow.timetables
@@ -724,36 +720,23 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
// airflow.utils.dates
["dates", "date_range"] => Replacement::None,
["dates", "days_ago"] => {
Replacement::Message("Use `pendulum.today('UTC').add(days=-N, ...)` instead")
}
["dates", "days_ago"] => Replacement::Name("pendulum.today('UTC').add(days=-N, ...)"),
["dates", "parse_execution_date" | "round_time" | "scale_time_units" | "infer_time_unit"] => {
Replacement::None
}
// airflow.utils.file
["file", "TemporaryDirectory"] => Replacement::AutoImport {
module: "tempfile",
name: "TemporaryDirectory",
},
["file", "mkdirs"] => Replacement::Message("Use `pathlib.Path({path}).mkdir` instead"),
["file", "TemporaryDirectory"] => Replacement::Name("tempfile.TemporaryDirectory"),
["file", "mkdirs"] => Replacement::Name("pathlib.Path({path}).mkdir"),
// airflow.utils.helpers
["helpers", "chain"] => Replacement::AutoImport {
module: "airflow.sdk",
name: "chain",
},
["helpers", "cross_downstream"] => Replacement::AutoImport {
module: "airflow.sdk",
name: "cross_downstream",
},
["helpers", "chain"] => Replacement::Name("airflow.sdk.chain"),
["helpers", "cross_downstream"] => Replacement::Name("airflow.sdk.cross_downstream"),
// TODO: update it as SourceModuleMoved
// airflow.utils.log.secrets_masker
["log", "secrets_masker"] => Replacement::AutoImport {
module: "airflow.sdk.execution_time",
name: "secrets_masker",
},
["log", "secrets_masker"] => {
Replacement::Name("airflow.sdk.execution_time.secrets_masker")
}
// airflow.utils.state
["state", "SHUTDOWN" | "terminating_states"] => Replacement::None,
@@ -768,20 +751,18 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
// airflow.www
// TODO: www has been removed
["airflow", "www", "auth", "has_access"] => {
Replacement::Message("Use `airflow.www.auth.has_access_*` instead")
Replacement::Name("airflow.www.auth.has_access_*")
}
["airflow", "www", "auth", "has_access_dataset"] => Replacement::AutoImport {
module: "airflow.www.auth",
name: "has_access_asset",
},
["airflow", "www", "utils", "get_sensitive_variables_fields"] => Replacement::AutoImport {
module: "airflow.utils.log.secrets_masker",
name: "get_sensitive_variables_fields",
},
["airflow", "www", "utils", "should_hide_value_for_key"] => Replacement::AutoImport {
module: "airflow.utils.log.secrets_masker",
name: "should_hide_value_for_key",
},
["airflow", "www", "utils", "get_sensitive_variables_fields"] => {
Replacement::Name("airflow.utils.log.secrets_masker.get_sensitive_variables_fields")
}
["airflow", "www", "utils", "should_hide_value_for_key"] => {
Replacement::Name("airflow.utils.log.secrets_masker.should_hide_value_for_key")
}
// airflow.providers.amazon
["airflow", "providers", "amazon", "aws", "datasets", "s3", rest] => match *rest {
@@ -793,10 +774,9 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.providers.amazon.aws.assets.s3",
name: "convert_asset_to_openlineage",
},
"sanitize_uri" => Replacement::AutoImport {
module: "airflow.providers.amazon.aws.assets.s3",
name: "sanitize_uri",
},
"sanitize_uri" => {
Replacement::Name("airflow.providers.amazon.aws.assets.s3.sanitize_uri")
}
_ => return,
},
["airflow", "providers", "amazon", "aws", "auth_manager", "avp", "entities", "AvpEntities", "DATASET"] => {
@@ -817,10 +797,9 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.providers.common.io.assets.file",
name: "convert_asset_to_openlineage",
},
"sanitize_uri" => Replacement::AutoImport {
module: "airflow.providers.common.io.assets.file",
name: "sanitize_uri",
},
"sanitize_uri" => {
Replacement::Name("airflow.providers.common.io.assets.file.sanitize_uri")
}
_ => return,
},
@@ -847,28 +826,20 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.providers.google.assets.gcs",
name: "convert_asset_to_openlineage",
},
["gcs", "sanitize_uri"] => Replacement::AutoImport {
module: "airflow.providers.google.assets.gcs",
name: "sanitize_uri",
},
["gcs", "sanitize_uri"] => {
Replacement::Name("airflow.providers.google.assets.gcs.sanitize_uri")
}
_ => return,
},
// airflow.providers.mysql
["airflow", "providers", "mysql", "datasets", "mysql", "sanitize_uri"] => {
Replacement::AutoImport {
module: "airflow.providers.mysql.assets.mysql",
name: "sanitize_uri",
}
Replacement::Name("airflow.providers.mysql.assets.mysql.sanitize_uri")
}
// airflow.providers.postgres
["airflow", "providers", "postgres", "datasets", "postgres", "sanitize_uri"] => {
Replacement::AutoImport {
module: "airflow.providers.postgres.assets.postgres",
name: "sanitize_uri",
}
Replacement::Name("airflow.providers.postgres.assets.postgres.sanitize_uri")
}
// airflow.providers.openlineage
@@ -888,15 +859,16 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
// airflow.providers.trino
["airflow", "providers", "trino", "datasets", "trino", "sanitize_uri"] => {
Replacement::AutoImport {
module: "airflow.providers.trino.assets.trino",
name: "sanitize_uri",
}
Replacement::Name("airflow.providers.trino.assets.trino.sanitize_uri")
}
_ => return,
};
if is_guarded_by_try_except(expr, &replacement, semantic) {
return;
}
let mut diagnostic = Diagnostic::new(
Airflow3Removal {
deprecated: qualified_name.to_string(),
@@ -904,15 +876,8 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
},
range,
);
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;
}
if let Replacement::AutoImport { module, name } = replacement {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, name),
@@ -981,7 +946,7 @@ fn diagnostic_for_argument(
Airflow3Removal {
deprecated: deprecated.to_string(),
replacement: match replacement {
Some(name) => Replacement::AttrName(name),
Some(name) => Replacement::Name(name),
None => Replacement::None,
},
},

View File

@@ -1,6 +1,6 @@
use crate::importer::ImportRequest;
use crate::rules::airflow::helpers::{is_guarded_by_try_except, ProviderReplacement};
use crate::rules::airflow::helpers::ProviderReplacement;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{Expr, ExprAttribute};
@@ -279,17 +279,13 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
ranged.range(),
);
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;
}
if let ProviderReplacement::AutoImport {
module,
name,
provider: _,
version: _,
} = replacement
{
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, name),

View File

@@ -53,8 +53,7 @@ impl Violation for Airflow3SuggestedUpdate {
} = self;
match replacement {
Replacement::None
| Replacement::AttrName(_)
| Replacement::Message(_)
| Replacement::Name(_)
| Replacement::AutoImport { module: _, name: _ }
| Replacement::SourceModuleMoved { module: _, name: _ } => {
format!(
@@ -62,21 +61,27 @@ impl Violation for Airflow3SuggestedUpdate {
It still works in Airflow 3.0 but is expected to be removed in a future version."
)
}
Replacement::Message(message) => {
format!(
"`{deprecated}` is removed in Airflow 3.0; \
It still works in Airflow 3.0 but is expected to be removed in a future version.; \
{message}"
)
}
}
}
fn fix_title(&self) -> Option<String> {
let Airflow3SuggestedUpdate { replacement, .. } = self;
match replacement {
Replacement::None => None,
Replacement::AttrName(name) => Some(format!("Use `{name}` instead")),
Replacement::Message(message) => Some((*message).to_string()),
Replacement::Name(name) => Some(format!("Use `{name}` instead")),
Replacement::AutoImport { module, name } => {
Some(format!("Use `{module}.{name}` instead"))
}
Replacement::SourceModuleMoved { module, name } => {
Some(format!("Use `{module}.{name}` instead"))
}
_ => None,
}
}
}
@@ -121,7 +126,7 @@ fn diagnostic_for_argument(
Airflow3SuggestedUpdate {
deprecated: deprecated.to_string(),
replacement: match replacement {
Some(name) => Replacement::AttrName(name),
Some(name) => Replacement::Name(name),
None => Replacement::None,
},
},
@@ -237,14 +242,6 @@ 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 {
@@ -278,6 +275,10 @@ 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(),
@@ -286,15 +287,7 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
range,
);
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;
}
if let Replacement::AutoImport { module, name } = replacement {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, name),

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0
AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
|
5 | name = "test_plugin"
6 | # --- Invalid extensions start
@@ -10,9 +10,8 @@ AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0
8 | sensors = [PluginSensorOperator]
9 | hooks = [PluginHook]
|
= help: This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0
AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
|
6 | # --- Invalid extensions start
7 | operators = [PluginOperator]
@@ -21,9 +20,8 @@ AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0
9 | hooks = [PluginHook]
10 | executors = [PluginExecutor]
|
= help: This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0
AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
|
7 | operators = [PluginOperator]
8 | sensors = [PluginSensorOperator]
@@ -32,9 +30,8 @@ AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0
10 | executors = [PluginExecutor]
11 | # --- Invalid extensions end
|
= help: This extension should just be imported as a regular python module.
AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0
AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
|
8 | sensors = [PluginSensorOperator]
9 | hooks = [PluginHook]
@@ -43,4 +40,3 @@ AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0
11 | # --- Invalid extensions end
12 | macros = [plugin_macro]
|
= help: This extension should just be imported as a regular python module.

View File

@@ -258,11 +258,10 @@ AIR301_args.py:90:16: AIR301 `filename_template` is removed in Airflow 3.0
92 | FabAuthManager(None)
|
AIR301_args.py:92:15: AIR301 `appbuilder` is removed in Airflow 3.0
AIR301_args.py:92:15: AIR301 `appbuilder` is removed in Airflow 3.0; The constructor takes no parameter now
|
90 | GCSTaskHandler(filename_template="/tmp/test")
91 |
92 | FabAuthManager(None)
| ^^^^^^ AIR301
|
= help: The constructor takes no parameter now

View File

@@ -1,488 +1,448 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR301_names.py:53:1: AIR301 `airflow.PY36` is removed in Airflow 3.0
AIR301_names.py:56:1: AIR301 `airflow.PY36` is removed in Airflow 3.0
|
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^ AIR301
54 |
55 | # airflow.api_connexion.security
57 |
58 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:53:7: AIR301 `airflow.PY37` is removed in Airflow 3.0
AIR301_names.py:56:7: AIR301 `airflow.PY37` is removed in Airflow 3.0
|
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^ AIR301
54 |
55 | # airflow.api_connexion.security
57 |
58 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:53:13: AIR301 `airflow.PY38` is removed in Airflow 3.0
AIR301_names.py:56:13: AIR301 `airflow.PY38` is removed in Airflow 3.0
|
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^ AIR301
54 |
55 | # airflow.api_connexion.security
57 |
58 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:53:19: AIR301 `airflow.PY39` is removed in Airflow 3.0
AIR301_names.py:56:19: AIR301 `airflow.PY39` is removed in Airflow 3.0
|
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^ AIR301
54 |
55 | # airflow.api_connexion.security
57 |
58 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:53:25: AIR301 `airflow.PY310` is removed in Airflow 3.0
AIR301_names.py:56:25: AIR301 `airflow.PY310` is removed in Airflow 3.0
|
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^ AIR301
54 |
55 | # airflow.api_connexion.security
57 |
58 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:53:32: AIR301 `airflow.PY311` is removed in Airflow 3.0
AIR301_names.py:56:32: AIR301 `airflow.PY311` is removed in Airflow 3.0
|
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^ AIR301
54 |
55 | # airflow.api_connexion.security
57 |
58 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:53:39: AIR301 `airflow.PY312` is removed in Airflow 3.0
AIR301_names.py:56:39: AIR301 `airflow.PY312` is removed in Airflow 3.0
|
52 | # airflow root
53 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
55 | # airflow root
56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^ AIR301
54 |
55 | # airflow.api_connexion.security
57 |
58 | # airflow.api_connexion.security
|
= help: Use `sys.version_info` instead
AIR301_names.py:56:1: AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0
AIR301_names.py:59:1: AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0
|
55 | # airflow.api_connexion.security
56 | requires_access
58 | # airflow.api_connexion.security
59 | requires_access
| ^^^^^^^^^^^^^^^ AIR301
|
= help: Use `airflow.api_connexion.security.requires_access_*` instead
AIR301_names.py:60:1: AIR301 `airflow.configuration.get` is removed in Airflow 3.0
AIR301_names.py:63:1: AIR301 `airflow.configuration.get` is removed in Airflow 3.0
|
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^ AIR301
|
= help: Use `airflow.configuration.conf.get` instead
AIR301_names.py:60:6: AIR301 `airflow.configuration.getboolean` is removed in Airflow 3.0
AIR301_names.py:63:6: AIR301 `airflow.configuration.getboolean` is removed in Airflow 3.0
|
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.getboolean` instead
AIR301_names.py:60:18: AIR301 `airflow.configuration.getfloat` is removed in Airflow 3.0
AIR301_names.py:63:18: AIR301 `airflow.configuration.getfloat` is removed in Airflow 3.0
|
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.getfloat` instead
AIR301_names.py:60:28: AIR301 `airflow.configuration.getint` is removed in Airflow 3.0
AIR301_names.py:63:28: AIR301 `airflow.configuration.getint` is removed in Airflow 3.0
|
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.getint` instead
AIR301_names.py:60:36: AIR301 `airflow.configuration.has_option` is removed in Airflow 3.0
AIR301_names.py:63:36: AIR301 `airflow.configuration.has_option` is removed in Airflow 3.0
|
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.has_option` instead
AIR301_names.py:60:48: AIR301 `airflow.configuration.remove_option` is removed in Airflow 3.0
AIR301_names.py:63:48: AIR301 `airflow.configuration.remove_option` is removed in Airflow 3.0
|
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.remove_option` instead
AIR301_names.py:60:63: AIR301 `airflow.configuration.as_dict` is removed in Airflow 3.0
AIR301_names.py:63:63: AIR301 `airflow.configuration.as_dict` is removed in Airflow 3.0
|
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^^^^^ AIR301
|
= help: Use `airflow.configuration.conf.as_dict` instead
AIR301_names.py:60:72: AIR301 `airflow.configuration.set` is removed in Airflow 3.0
AIR301_names.py:63:72: AIR301 `airflow.configuration.set` is removed in Airflow 3.0
|
59 | # airflow.configuration
60 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
62 | # airflow.configuration
63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
| ^^^ AIR301
|
= help: Use `airflow.configuration.conf.set` instead
AIR301_names.py:64:1: AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0
AIR301_names.py:67:1: AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0; The whole `airflow.contrib` module has been removed.
|
63 | # airflow.contrib.*
64 | AWSAthenaHook()
66 | # airflow.contrib.*
67 | AWSAthenaHook()
| ^^^^^^^^^^^^^ AIR301
|
= help: The whole `airflow.contrib` module has been removed.
AIR301_names.py:68:1: AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
AIR301_names.py:71:1: AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
|
67 | # airflow.datasets
68 | DatasetAliasEvent()
70 | # airflow.datasets
71 | DatasetAliasEvent()
| ^^^^^^^^^^^^^^^^^ AIR301
|
AIR301_names.py:72:1: AIR301 `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0
AIR301_names.py:75:1: AIR301 `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0
|
71 | # airflow.hooks
72 | BaseHook()
74 | # airflow.hooks
75 | BaseHook()
| ^^^^^^^^ AIR301
|
= help: Use `airflow.hooks.base.BaseHook` instead
AIR301_names.py:76:1: AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
AIR301_names.py:79:1: AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0; The whole `airflow.subdag` module has been removed.
|
75 | # airflow.operators.subdag.*
76 | SubDagOperator()
78 | # airflow.operators.subdag.*
79 | SubDagOperator()
| ^^^^^^^^^^^^^^ AIR301
80 |
81 | # airflow.providers.mysql
|
= help: The whole `airflow.subdag` module has been removed.
AIR301_names.py:85:1: AIR301 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0
AIR301_names.py:82:7: AIR301 `airflow.providers.mysql.datasets.mysql.sanitize_uri` is removed in Airflow 3.0
|
84 | # airflow.sensors.base_sensor_operator
85 | BaseSensorOperator()
81 | # airflow.providers.mysql
82 | mysql.sanitize_uri
| ^^^^^^^^^^^^ AIR301
83 |
84 | # airflow.providers.postgres
|
= help: Use `airflow.providers.mysql.assets.mysql.sanitize_uri` instead
AIR301_names.py:85:10: AIR301 `airflow.providers.postgres.datasets.postgres.sanitize_uri` is removed in Airflow 3.0
|
84 | # airflow.providers.postgres
85 | postgres.sanitize_uri
| ^^^^^^^^^^^^ AIR301
86 |
87 | # airflow.providers.trino
|
= help: Use `airflow.providers.postgres.assets.postgres.sanitize_uri` instead
AIR301_names.py:88:7: AIR301 `airflow.providers.trino.datasets.trino.sanitize_uri` is removed in Airflow 3.0
|
87 | # airflow.providers.trino
88 | trino.sanitize_uri
| ^^^^^^^^^^^^ AIR301
89 |
90 | # airflow.secrets
|
= help: Use `airflow.providers.trino.assets.trino.sanitize_uri` instead
AIR301_names.py:96:1: AIR301 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0
|
95 | # airflow.sensors.base_sensor_operator
96 | BaseSensorOperator()
| ^^^^^^^^^^^^^^^^^^ AIR301
|
= help: Use `airflow.sdk.bases.sensor.BaseSensorOperator` instead
AIR301_names.py:89:1: AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
|
88 | # airflow.triggers.external_task
89 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^ AIR301
90 |
91 | # airflow.utils.date
|
AIR301_names.py:92:7: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
91 | # airflow.utils.date
92 | dates.date_range
| ^^^^^^^^^^ AIR301
93 | dates.days_ago
|
AIR301_names.py:93:7: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
|
91 | # airflow.utils.date
92 | dates.date_range
93 | dates.days_ago
| ^^^^^^^^ AIR301
94 |
95 | date_range
|
= help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301_names.py:95:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
93 | dates.days_ago
94 |
95 | date_range
| ^^^^^^^^^^ AIR301
96 | days_ago
97 | infer_time_unit
|
AIR301_names.py:96:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
|
95 | date_range
96 | days_ago
| ^^^^^^^^ AIR301
97 | infer_time_unit
98 | parse_execution_date
|
= help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301_names.py:97:1: AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
|
95 | date_range
96 | days_ago
97 | infer_time_unit
| ^^^^^^^^^^^^^^^ AIR301
98 | parse_execution_date
99 | round_time
|
AIR301_names.py:98:1: AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
AIR301_names.py:100:1: AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
|
96 | days_ago
97 | infer_time_unit
98 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^ AIR301
99 | round_time
100 | scale_time_units
|
AIR301_names.py:99:1: AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
|
97 | infer_time_unit
98 | parse_execution_date
99 | round_time
| ^^^^^^^^^^ AIR301
100 | scale_time_units
|
AIR301_names.py:100:1: AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
|
98 | parse_execution_date
99 | round_time
100 | scale_time_units
99 | # airflow.triggers.external_task
100 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^ AIR301
101 |
102 | # This one was not deprecated.
102 | # airflow.utils.date
|
AIR301_names.py:107:1: AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
AIR301_names.py:103:7: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
106 | # airflow.utils.dag_cycle_tester
107 | test_cycle
102 | # airflow.utils.date
103 | dates.date_range
| ^^^^^^^^^^ AIR301
104 | dates.days_ago
|
AIR301_names.py:104:7: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
|
102 | # airflow.utils.date
103 | dates.date_range
104 | dates.days_ago
| ^^^^^^^^ AIR301
105 |
106 | date_range
|
= help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301_names.py:106:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
104 | dates.days_ago
105 |
106 | date_range
| ^^^^^^^^^^ AIR301
107 | days_ago
108 | infer_time_unit
|
AIR301_names.py:107:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
|
106 | date_range
107 | days_ago
| ^^^^^^^^ AIR301
108 | infer_time_unit
109 | parse_execution_date
|
= help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301_names.py:108:1: AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
|
106 | date_range
107 | days_ago
108 | infer_time_unit
| ^^^^^^^^^^^^^^^ AIR301
109 | parse_execution_date
110 | round_time
|
AIR301_names.py:109:1: AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
|
107 | days_ago
108 | infer_time_unit
109 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^ AIR301
110 | round_time
111 | scale_time_units
|
AIR301_names.py:110:1: AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
|
108 | infer_time_unit
109 | parse_execution_date
110 | round_time
| ^^^^^^^^^^ AIR301
111 | scale_time_units
|
AIR301_names.py:111:1: AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
|
109 | parse_execution_date
110 | round_time
111 | scale_time_units
| ^^^^^^^^^^^^^^^^ AIR301
112 |
113 | # This one was not deprecated.
|
AIR301_names.py:118:1: AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
|
117 | # airflow.utils.dag_cycle_tester
118 | test_cycle
| ^^^^^^^^^^ AIR301
|
AIR301_names.py:111:1: AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
AIR301_names.py:122:1: AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
|
110 | # airflow.utils.db
111 | create_session
121 | # airflow.utils.db
122 | create_session
| ^^^^^^^^^^^^^^ AIR301
112 |
113 | # airflow.utils.decorators
123 |
124 | # airflow.utils.decorators
|
AIR301_names.py:114:1: AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0
AIR301_names.py:125:1: AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0; `apply_defaults` is now unconditionally done and can be safely removed.
|
113 | # airflow.utils.decorators
114 | apply_defaults
124 | # airflow.utils.decorators
125 | apply_defaults
| ^^^^^^^^^^^^^^ AIR301
115 |
116 | # airflow.utils.file
126 |
127 | # airflow.utils.file
|
= help: `apply_defaults` is now unconditionally done and can be safely removed.
AIR301_names.py:117:1: AIR301 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0
AIR301_names.py:128:1: AIR301 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0
|
116 | # airflow.utils.file
117 | TemporaryDirectory()
127 | # airflow.utils.file
128 | TemporaryDirectory()
| ^^^^^^^^^^^^^^^^^^ AIR301
118 | mkdirs
129 | mkdirs
|
= help: Use `tempfile.TemporaryDirectory` instead
AIR301_names.py:118:1: AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
AIR301_names.py:129:1: AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
|
116 | # airflow.utils.file
117 | TemporaryDirectory()
118 | mkdirs
127 | # airflow.utils.file
128 | TemporaryDirectory()
129 | mkdirs
| ^^^^^^ AIR301
119 |
120 | # airflow.utils.helpers
130 |
131 | # airflow.utils.helpers
|
= help: Use `pathlib.Path({path}).mkdir` instead
AIR301_names.py:121:1: AIR301 [*] `airflow.utils.helpers.chain` is removed in Airflow 3.0
AIR301_names.py:132:1: AIR301 `airflow.utils.helpers.chain` is removed in Airflow 3.0
|
120 | # airflow.utils.helpers
121 | helper_chain
131 | # airflow.utils.helpers
132 | helper_chain
| ^^^^^^^^^^^^ AIR301
122 | helper_cross_downstream
133 | helper_cross_downstream
|
= help: Use `airflow.sdk.chain` instead
Safe fix
48 48 | from airflow.utils.trigger_rule import TriggerRule
49 49 | from airflow.www.auth import has_access
50 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
51 |+from airflow.sdk import chain
51 52 |
52 53 | # airflow root
53 54 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
--------------------------------------------------------------------------------
118 119 | mkdirs
119 120 |
120 121 | # airflow.utils.helpers
121 |-helper_chain
122 |+chain
122 123 | helper_cross_downstream
123 124 |
124 125 | # airflow.utils.log
AIR301_names.py:122:1: AIR301 [*] `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0
AIR301_names.py:133:1: AIR301 `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0
|
120 | # airflow.utils.helpers
121 | helper_chain
122 | helper_cross_downstream
131 | # airflow.utils.helpers
132 | helper_chain
133 | helper_cross_downstream
| ^^^^^^^^^^^^^^^^^^^^^^^ AIR301
123 |
124 | # airflow.utils.log
134 |
135 | # airflow.utils.log
|
= help: Use `airflow.sdk.cross_downstream` instead
Safe fix
48 48 | from airflow.utils.trigger_rule import TriggerRule
49 49 | from airflow.www.auth import has_access
50 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
51 |+from airflow.sdk import cross_downstream
51 52 |
52 53 | # airflow root
53 54 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
--------------------------------------------------------------------------------
119 120 |
120 121 | # airflow.utils.helpers
121 122 | helper_chain
122 |-helper_cross_downstream
123 |+cross_downstream
123 124 |
124 125 | # airflow.utils.log
125 126 | secrets_masker
AIR301_names.py:125:1: AIR301 `airflow.utils.log.secrets_masker` is removed in Airflow 3.0
AIR301_names.py:136:1: AIR301 `airflow.utils.log.secrets_masker` is removed in Airflow 3.0
|
124 | # airflow.utils.log
125 | secrets_masker
135 | # airflow.utils.log
136 | secrets_masker
| ^^^^^^^^^^^^^^ AIR301
126 |
127 | # airflow.utils.state
137 |
138 | # airflow.utils.state
|
= help: Use `airflow.sdk.execution_time.secrets_masker` instead
AIR301_names.py:128:1: AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
AIR301_names.py:139:1: AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
|
127 | # airflow.utils.state
128 | SHUTDOWN
138 | # airflow.utils.state
139 | SHUTDOWN
| ^^^^^^^^ AIR301
129 | terminating_states
140 | terminating_states
|
AIR301_names.py:129:1: AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
AIR301_names.py:140:1: AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
|
127 | # airflow.utils.state
128 | SHUTDOWN
129 | terminating_states
138 | # airflow.utils.state
139 | SHUTDOWN
140 | terminating_states
| ^^^^^^^^^^^^^^^^^^ AIR301
130 |
131 | # airflow.utils.trigger_rule
141 |
142 | # airflow.utils.trigger_rule
|
AIR301_names.py:132:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
AIR301_names.py:143:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
|
131 | # airflow.utils.trigger_rule
132 | TriggerRule.DUMMY
142 | # airflow.utils.trigger_rule
143 | TriggerRule.DUMMY
| ^^^^^ AIR301
133 | TriggerRule.NONE_FAILED_OR_SKIPPED
144 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
AIR301_names.py:133:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
AIR301_names.py:144:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
|
131 | # airflow.utils.trigger_rule
132 | TriggerRule.DUMMY
133 | TriggerRule.NONE_FAILED_OR_SKIPPED
142 | # airflow.utils.trigger_rule
143 | TriggerRule.DUMMY
144 | TriggerRule.NONE_FAILED_OR_SKIPPED
| ^^^^^^^^^^^^^^^^^^^^^^ AIR301
|
AIR301_names.py:137:1: AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
AIR301_names.py:148:1: AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
|
136 | # airflow.www.auth
137 | has_access
147 | # airflow.www.auth
148 | has_access
| ^^^^^^^^^^ AIR301
138 |
139 | # airflow.www.utils
149 |
150 | # airflow.www.utils
|
= help: Use `airflow.www.auth.has_access_*` instead
AIR301_names.py:140:1: AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
AIR301_names.py:151:1: AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
|
139 | # airflow.www.utils
140 | get_sensitive_variables_fields
150 | # airflow.www.utils
151 | get_sensitive_variables_fields
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301
141 | should_hide_value_for_key
152 | should_hide_value_for_key
|
= help: Use `airflow.utils.log.secrets_masker.get_sensitive_variables_fields` instead
AIR301_names.py:141:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
AIR301_names.py:152:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
|
139 | # airflow.www.utils
140 | get_sensitive_variables_fields
141 | should_hide_value_for_key
150 | # airflow.www.utils
151 | get_sensitive_variables_fields
152 | should_hide_value_for_key
| ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301
142 |
143 | # airflow.operators.python
153 |
154 | # airflow.operators.python
|
= help: Use `airflow.utils.log.secrets_masker.should_hide_value_for_key` instead
AIR301_names.py:146:1: AIR301 `airflow.operators.python.get_current_context` is removed in Airflow 3.0
AIR301_names.py:157:1: AIR301 `airflow.operators.python.get_current_context` is removed in Airflow 3.0
|
144 | from airflow.operators.python import get_current_context
145 |
146 | get_current_context()
155 | from airflow.operators.python import get_current_context
156 |
157 | get_current_context()
| ^^^^^^^^^^^^^^^^^^^ AIR301
147 |
148 | # airflow.providers.mysql
|
= help: Use `airflow.sdk.get_current_context` instead
AIR301_names.py:151:1: AIR301 `airflow.providers.mysql.datasets.mysql.sanitize_uri` is removed in Airflow 3.0
|
149 | from airflow.providers.mysql.datasets.mysql import sanitize_uri
150 |
151 | sanitize_uri
| ^^^^^^^^^^^^ AIR301
152 |
153 | # airflow.providers.postgres
|
= help: Use `airflow.providers.mysql.assets.mysql.sanitize_uri` instead
AIR301_names.py:156:1: AIR301 `airflow.providers.postgres.datasets.postgres.sanitize_uri` is removed in Airflow 3.0
|
154 | from airflow.providers.postgres.datasets.postgres import sanitize_uri
155 |
156 | sanitize_uri
| ^^^^^^^^^^^^ AIR301
157 |
158 | # airflow.providers.trino
|
= help: Use `airflow.providers.postgres.assets.postgres.sanitize_uri` instead
AIR301_names.py:161:1: AIR301 `airflow.providers.trino.datasets.trino.sanitize_uri` is removed in Airflow 3.0
|
159 | from airflow.providers.trino.datasets.trino import sanitize_uri
160 |
161 | sanitize_uri
| ^^^^^^^^^^^^ AIR301
|
= help: Use `airflow.providers.trino.assets.trino.sanitize_uri` instead

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---

View File

@@ -1,394 +1,337 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
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.
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.
|
26 | # airflow
27 | DatasetFromRoot()
22 | # airflow
23 | DatasetFromRoot()
| ^^^^^^^^^^^^^^^ AIR311
28 |
29 | # airflow.datasets
24 |
25 | # airflow.datasets
|
= help: Use `airflow.sdk.Asset` instead
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 Asset
25 26 |
26 27 | # airflow
27 |-DatasetFromRoot()
28 |+Asset()
28 29 |
29 30 | # airflow.datasets
30 31 | Dataset()
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()
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.
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.
|
29 | # airflow.datasets
30 | Dataset()
25 | # airflow.datasets
26 | Dataset()
| ^^^^^^^ AIR311
31 | DatasetAlias()
32 | DatasetAll()
27 | DatasetAlias()
28 | DatasetAll()
|
= help: Use `airflow.sdk.Asset` instead
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 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()
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()
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.
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.
|
29 | # airflow.datasets
30 | Dataset()
31 | DatasetAlias()
25 | # airflow.datasets
26 | Dataset()
27 | DatasetAlias()
| ^^^^^^^^^^^^ AIR311
32 | DatasetAll()
33 | DatasetAny()
28 | DatasetAll()
29 | DatasetAny()
|
= help: Use `airflow.sdk.AssetAlias` instead
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 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()
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()
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.
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.
|
30 | Dataset()
31 | DatasetAlias()
32 | DatasetAll()
26 | Dataset()
27 | DatasetAlias()
28 | DatasetAll()
| ^^^^^^^^^^ AIR311
33 | DatasetAny()
34 | Metadata()
29 | DatasetAny()
30 | Metadata()
|
= help: Use `airflow.sdk.AssetAll` instead
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 AssetAll
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
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()
--------------------------------------------------------------------------------
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()
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()
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.
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.
|
31 | DatasetAlias()
32 | DatasetAll()
33 | DatasetAny()
27 | DatasetAlias()
28 | DatasetAll()
29 | DatasetAny()
| ^^^^^^^^^^ AIR311
34 | Metadata()
35 | expand_alias_to_datasets()
30 | Metadata()
31 | expand_alias_to_datasets()
|
= help: Use `airflow.sdk.AssetAny` instead
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 AssetAny
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
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()
--------------------------------------------------------------------------------
30 31 | Dataset()
31 32 | DatasetAlias()
32 33 | DatasetAll()
33 |-DatasetAny()
34 |+AssetAny()
34 35 | Metadata()
35 36 | expand_alias_to_datasets()
36 37 |
26 27 | Dataset()
27 28 | DatasetAlias()
28 29 | DatasetAll()
29 |-DatasetAny()
30 |+AssetAny()
30 31 | Metadata()
31 32 | expand_alias_to_datasets()
32 33 |
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.
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.
|
32 | DatasetAll()
33 | DatasetAny()
34 | Metadata()
28 | DatasetAll()
29 | DatasetAny()
30 | Metadata()
| ^^^^^^^^ AIR311
35 | expand_alias_to_datasets()
31 | expand_alias_to_datasets()
|
= help: Use `airflow.sdk.Metadata` instead
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.
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.
|
33 | DatasetAny()
34 | Metadata()
35 | expand_alias_to_datasets()
29 | DatasetAny()
30 | Metadata()
31 | expand_alias_to_datasets()
| ^^^^^^^^^^^^^^^^^^^^^^^^ AIR311
36 |
37 | # airflow.decorators
32 |
33 | # airflow.decorators
|
= help: Use `airflow.sdk.expand_alias_to_assets` instead
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 expand_alias_to_assets
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
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()
--------------------------------------------------------------------------------
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()
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()
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.
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.
|
37 | # airflow.decorators
38 | dag()
33 | # airflow.decorators
34 | dag()
| ^^^ AIR311
39 | task()
40 | task_group()
35 | task()
36 | task_group()
|
= help: Use `airflow.sdk.dag` instead
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.
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.
|
37 | # airflow.decorators
38 | dag()
39 | task()
33 | # airflow.decorators
34 | dag()
35 | task()
| ^^^^ AIR311
40 | task_group()
41 | setup()
36 | task_group()
37 | setup()
|
= help: Use `airflow.sdk.task` instead
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.
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.
|
38 | dag()
39 | task()
40 | task_group()
34 | dag()
35 | task()
36 | task_group()
| ^^^^^^^^^^ AIR311
41 | setup()
42 | teardown()
37 | setup()
38 | teardown()
|
= help: Use `airflow.sdk.task_group` instead
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.
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.
|
39 | task()
40 | task_group()
41 | setup()
35 | task()
36 | task_group()
37 | setup()
| ^^^^^ AIR311
42 | teardown()
38 | teardown()
|
= help: Use `airflow.sdk.setup` instead
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.
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.
|
40 | task_group()
41 | setup()
42 | teardown()
36 | task_group()
37 | setup()
38 | teardown()
| ^^^^^^^^ AIR311
43 |
44 | # airflow.io
39 |
40 | # airflow.io
|
= help: Use `airflow.sdk.teardown` instead
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.
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.
|
44 | # airflow.io
45 | ObjectStoragePath()
40 | # airflow.io
41 | ObjectStoragePath()
| ^^^^^^^^^^^^^^^^^ AIR311
46 | attach()
42 | attach()
|
= help: Use `airflow.sdk.ObjectStoragePath` instead
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.
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.
|
44 | # airflow.io
45 | ObjectStoragePath()
46 | attach()
40 | # airflow.io
41 | ObjectStoragePath()
42 | attach()
| ^^^^^^ AIR311
47 |
48 | # airflow.models
43 |
44 | # airflow.models
|
= help: Use `airflow.sdk.io.attach` instead
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.
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.
|
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()
44 | # airflow.models
45 | DAGFromModel()
| ^^^^^^^^^^^^ AIR311
51 | Variable()
46 |
47 | # airflow.models.baseoperator
|
= help: Use `airflow.sdk.DAG` instead
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.
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.
|
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()
47 | # airflow.models.baseoperator
48 | chain()
| ^^^^^ AIR311
55 | chain_linear()
56 | cross_downstream()
49 | chain_linear()
50 | cross_downstream()
|
= help: Use `airflow.sdk.chain` instead
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.
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.
|
53 | # airflow.models.baseoperator
54 | chain()
55 | chain_linear()
47 | # airflow.models.baseoperator
48 | chain()
49 | chain_linear()
| ^^^^^^^^^^^^ AIR311
56 | cross_downstream()
50 | cross_downstream()
|
= help: Use `airflow.sdk.chain_linear` instead
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.
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.
|
54 | chain()
55 | chain_linear()
56 | cross_downstream()
48 | chain()
49 | chain_linear()
50 | cross_downstream()
| ^^^^^^^^^^^^^^^^ AIR311
57 |
58 | # airflow.models.baseoperatolinker
51 |
52 | # airflow.models.baseoperatolinker
|
= help: Use `airflow.sdk.cross_downstream` instead
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.
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.
|
61 | # airflow.models.dag
62 | DAGFromDag()
55 | # airflow.models.dag
56 | DAGFromDag()
| ^^^^^^^^^^ AIR311
63 | # airflow.timetables.datasets
64 | DatasetOrTimeSchedule()
57 | # airflow.timetables.datasets
58 | DatasetOrTimeSchedule()
|
= help: Use `airflow.sdk.DAG` instead
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.
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.
|
62 | DAGFromDag()
63 | # airflow.timetables.datasets
64 | DatasetOrTimeSchedule()
56 | DAGFromDag()
57 | # airflow.timetables.datasets
58 | DatasetOrTimeSchedule()
| ^^^^^^^^^^^^^^^^^^^^^ AIR311
65 |
66 | # airflow.utils.dag_parsing_context
59 |
60 | # airflow.utils.dag_parsing_context
|
= help: Use `airflow.timetables.assets.AssetOrTimeSchedule` instead
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.timetables.assets import AssetOrTimeSchedule
25 26 |
26 27 | # airflow
27 28 | DatasetFromRoot()
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()
--------------------------------------------------------------------------------
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()
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()
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.
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.
|
66 | # airflow.utils.dag_parsing_context
67 | get_parsing_context()
60 | # airflow.utils.dag_parsing_context
61 | get_parsing_context()
| ^^^^^^^^^^^^^^^^^^^ AIR311
|
= help: Use `airflow.sdk.get_parsing_context` instead

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---

View File

@@ -100,15 +100,7 @@ pub(crate) fn hardcoded_sql_expression(checker: &Checker, expr: &Expr) {
}
// f"select * from table where val = {val}"
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())
}
Expr::FString(f_string) => concatenated_f_string(f_string, checker.locator()),
_ => return,
};

View File

@@ -601,6 +601,4 @@ 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
|

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
use crate::{checkers::ast::Checker, fix::edits::add_argument};
@@ -60,18 +60,10 @@ 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
@@ -98,14 +90,3 @@ 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,
})
}

View File

@@ -61,26 +61,3 @@ 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)

View File

@@ -1,7 +1,7 @@
use itertools::Itertools;
use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_diagnostics::{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,7 +19,6 @@ use crate::fix::edits::{remove_argument, Parentheses};
/// arguments directly.
///
/// ## Example
///
/// ```python
/// def foo(bar):
/// return bar + 1
@@ -29,7 +28,6 @@ use crate::fix::edits::{remove_argument, Parentheses};
/// ```
///
/// Use instead:
///
/// ```python
/// def foo(bar):
/// return bar + 1
@@ -38,26 +36,6 @@ 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)
@@ -135,7 +113,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
.iter()
.all(|kwarg| !duplicate_keywords.contains(kwarg))
{
let edit = Edit::range_replacement(
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
kwargs
.iter()
.zip(dict.iter_values())
@@ -156,15 +134,7 @@ 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
},
));
)));
}
}
}

View File

@@ -208,8 +208,6 @@ 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
|
@@ -217,8 +215,6 @@ 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
@@ -228,34 +224,3 @@ 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 | )

View File

@@ -1,7 +1,6 @@
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::name::UnqualifiedName;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
@@ -12,7 +11,6 @@ use ruff_python_semantic::SemanticModel;
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
use ruff_text_size::{TextLen, TextRange};
use rustc_hash::FxHashSet;
use crate::checkers::ast::Checker;
use crate::fix::edits;
@@ -809,51 +807,10 @@ fn check_fixture_returns(checker: &Checker, name: &str, body: &[Stmt], returns:
}
/// PT019
fn check_test_function_args(checker: &Checker, parameters: &Parameters, decorators: &[Decorator]) {
let mut named_parametrize = FxHashSet::default();
for decorator in decorators.iter().filter(|decorator| {
UnqualifiedName::from_expr(map_callable(&decorator.expression))
.is_some_and(|name| matches!(name.segments(), ["pytest", "mark", "parametrize"]))
}) {
let Some(call_expr) = decorator.expression.as_call_expr() else {
continue;
};
let Some(first_arg) = call_expr.arguments.find_argument_value("argnames", 0) else {
continue;
};
match first_arg {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
named_parametrize.extend(
value
.to_str()
.split(',')
.map(str::trim)
.filter(|param| !param.is_empty() && param.starts_with('_')),
);
}
Expr::Name(_) => return,
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. })
if elts.iter().any(Expr::is_name_expr) =>
{
return
}
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
named_parametrize.extend(
elts.iter()
.filter_map(Expr::as_string_literal_expr)
.map(|param| param.value.to_str().trim())
.filter(|param| !param.is_empty() && param.starts_with('_')),
);
}
_ => {}
}
}
fn check_test_function_args(checker: &Checker, parameters: &Parameters) {
for parameter in parameters.iter_non_variadic_params() {
let name = parameter.name();
if name.starts_with('_') && !named_parametrize.contains(name.as_str()) {
if name.starts_with('_') {
checker.report_diagnostic(Diagnostic::new(
PytestFixtureParamWithoutValue {
name: name.to_string(),
@@ -958,6 +915,6 @@ pub(crate) fn fixture(
}
if checker.enabled(Rule::PytestFixtureParamWithoutValue) && name.starts_with("test_") {
check_test_function_args(checker, parameters, decorators);
check_test_function_args(checker, parameters);
}
}

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT019.py:9:14: PT019 Fixture `_fixture` without value is injected as parameter, use `@pytest.mark.usefixtures` instead
|
@@ -14,19 +15,3 @@ PT019.py:13:17: PT019 Fixture `_fixture` without value is injected as parameter,
| ^^^^^^^^ PT019
14 | pass
|
PT019.py:31:24: PT019 Fixture `_bar` without value is injected as parameter, use `@pytest.mark.usefixtures` instead
|
30 | @pytest.mark.parametrize("_foo", [1, 2, 3])
31 | def test_thingy2(_foo, _bar): # Error _bar is not defined in parametrize
| ^^^^ PT019
32 | pass
|
PT019.py:39:24: PT019 Fixture `_bar` without value is injected as parameter, use `@pytest.mark.usefixtures` instead
|
38 | @pytest.mark.parametrize(("_foo"), [1, 2, 3])
39 | def test_thingy4(_foo, _bar): # Error _bar is not defined in parametrize
| ^^^^ PT019
40 | pass
|

View File

@@ -83,7 +83,7 @@ pub(crate) fn split_static_string(
let sep_arg = arguments.find_argument_value("sep", 0);
let split_replacement = if let Some(sep) = sep_arg {
match sep {
Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value, direction),
Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value),
Expr::StringLiteral(sep_value) => {
let sep_value_str = sep_value.value.to_str();
Some(split_sep(
@@ -99,7 +99,7 @@ pub(crate) fn split_static_string(
}
}
} else {
split_default(str_value, maxsplit_value, direction)
split_default(str_value, maxsplit_value)
};
let mut diagnostic = Diagnostic::new(SplitStaticString, call.range());
@@ -144,11 +144,7 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
})
}
fn split_default(
str_value: &StringLiteralValue,
max_split: i32,
direction: Direction,
) -> Option<Expr> {
fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option<Expr> {
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
@@ -156,7 +152,6 @@ fn split_default(
// > Consequently, splitting an empty string or a string consisting of just whitespace with
// > a None separator returns [].
// https://docs.python.org/3/library/stdtypes.html#str.split
let string_val = str_value.to_str();
match max_split.cmp(&0) {
Ordering::Greater => {
// Autofix for `maxsplit` without separator not yet implemented, as
@@ -165,30 +160,14 @@ fn split_default(
None
}
Ordering::Equal => {
// Behavior for maxsplit = 0 when sep is None:
// - If the string is empty or all whitespace, result is [].
// - Otherwise:
// - " x ".split(maxsplit=0) -> ['x ']
// - " x ".rsplit(maxsplit=0) -> [' x']
// - "".split(maxsplit=0) -> []
// - " ".split(maxsplit=0) -> []
let processed_str = if direction == Direction::Left {
string_val.trim_start()
} else {
string_val.trim_end()
};
let list_items: &[_] = if processed_str.is_empty() {
&[]
} else {
&[processed_str]
};
let list_items: Vec<&str> = vec![str_value.to_str()];
Some(construct_replacement(
list_items,
&list_items,
str_value.first_literal_flags(),
))
}
Ordering::Less => {
let list_items: Vec<&str> = string_val.split_whitespace().collect();
let list_items: Vec<&str> = str_value.to_str().split_whitespace().collect();
Some(construct_replacement(
&list_items,
str_value.first_literal_flags(),
@@ -207,20 +186,12 @@ 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 => {
let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect();
items.reverse();
items
}
Direction::Right => value.rsplitn(split_n + 1, sep_value).collect(),
}
} else {
match direction {
Direction::Left => value.split(sep_value).collect(),
Direction::Right => {
let mut items: Vec<&str> = value.rsplit(sep_value).collect();
items.reverse();
items
}
Direction::Right => value.rsplit(sep_value).collect(),
}
};

View File

@@ -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,8 +854,6 @@ 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
@@ -870,393 +868,3 @@ 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))
113 113 |
114 114 | # https://github.com/astral-sh/ruff/issues/18069
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
113 |
114 | # https://github.com/astral-sh/ruff/issues/18069
|
= 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"])
113 113 |
114 114 | # https://github.com/astral-sh/ruff/issues/18069
115 115 |
SIM905.py:116:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
114 | # https://github.com/astral-sh/ruff/issues/18069
115 |
116 | print("".split(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^ SIM905
117 | print("".split(sep=None, maxsplit=0))
118 | print(" ".split(maxsplit=0))
|
= help: Replace with list literal
Safe fix
113 113 |
114 114 | # https://github.com/astral-sh/ruff/issues/18069
115 115 |
116 |-print("".split(maxsplit=0))
116 |+print([])
117 117 | print("".split(sep=None, maxsplit=0))
118 118 | print(" ".split(maxsplit=0))
119 119 | print(" ".split(sep=None, maxsplit=0))
SIM905.py:117:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
116 | print("".split(maxsplit=0))
117 | print("".split(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
118 | print(" ".split(maxsplit=0))
119 | print(" ".split(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
114 114 | # https://github.com/astral-sh/ruff/issues/18069
115 115 |
116 116 | print("".split(maxsplit=0))
117 |-print("".split(sep=None, maxsplit=0))
117 |+print([])
118 118 | print(" ".split(maxsplit=0))
119 119 | print(" ".split(sep=None, maxsplit=0))
120 120 | print(" x ".split(maxsplit=0))
SIM905.py:118:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
116 | print("".split(maxsplit=0))
117 | print("".split(sep=None, maxsplit=0))
118 | print(" ".split(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^ SIM905
119 | print(" ".split(sep=None, maxsplit=0))
120 | print(" x ".split(maxsplit=0))
|
= help: Replace with list literal
Safe fix
115 115 |
116 116 | print("".split(maxsplit=0))
117 117 | print("".split(sep=None, maxsplit=0))
118 |-print(" ".split(maxsplit=0))
118 |+print([])
119 119 | print(" ".split(sep=None, maxsplit=0))
120 120 | print(" x ".split(maxsplit=0))
121 121 | print(" x ".split(sep=None, maxsplit=0))
SIM905.py:119:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
117 | print("".split(sep=None, maxsplit=0))
118 | print(" ".split(maxsplit=0))
119 | print(" ".split(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
120 | print(" x ".split(maxsplit=0))
121 | print(" x ".split(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
116 116 | print("".split(maxsplit=0))
117 117 | print("".split(sep=None, maxsplit=0))
118 118 | print(" ".split(maxsplit=0))
119 |-print(" ".split(sep=None, maxsplit=0))
119 |+print([])
120 120 | print(" x ".split(maxsplit=0))
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 122 | print(" x ".split(maxsplit=0))
SIM905.py:120:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
118 | print(" ".split(maxsplit=0))
119 | print(" ".split(sep=None, maxsplit=0))
120 | print(" x ".split(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM905
121 | print(" x ".split(sep=None, maxsplit=0))
122 | print(" x ".split(maxsplit=0))
|
= help: Replace with list literal
Safe fix
117 117 | print("".split(sep=None, maxsplit=0))
118 118 | print(" ".split(maxsplit=0))
119 119 | print(" ".split(sep=None, maxsplit=0))
120 |-print(" x ".split(maxsplit=0))
120 |+print(["x "])
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 122 | print(" x ".split(maxsplit=0))
123 123 | print(" x ".split(sep=None, maxsplit=0))
SIM905.py:121:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
119 | print(" ".split(sep=None, maxsplit=0))
120 | print(" x ".split(maxsplit=0))
121 | print(" x ".split(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
122 | print(" x ".split(maxsplit=0))
123 | print(" x ".split(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
118 118 | print(" ".split(maxsplit=0))
119 119 | print(" ".split(sep=None, maxsplit=0))
120 120 | print(" x ".split(maxsplit=0))
121 |-print(" x ".split(sep=None, maxsplit=0))
121 |+print(["x "])
122 122 | print(" x ".split(maxsplit=0))
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 124 | print("".rsplit(maxsplit=0))
SIM905.py:122:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
120 | print(" x ".split(maxsplit=0))
121 | print(" x ".split(sep=None, maxsplit=0))
122 | print(" x ".split(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
123 | print(" x ".split(sep=None, maxsplit=0))
124 | print("".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
119 119 | print(" ".split(sep=None, maxsplit=0))
120 120 | print(" x ".split(maxsplit=0))
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 |-print(" x ".split(maxsplit=0))
122 |+print(["x "])
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 124 | print("".rsplit(maxsplit=0))
125 125 | print("".rsplit(sep=None, maxsplit=0))
SIM905.py:123:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
121 | print(" x ".split(sep=None, maxsplit=0))
122 | print(" x ".split(maxsplit=0))
123 | print(" x ".split(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
124 | print("".rsplit(maxsplit=0))
125 | print("".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
120 120 | print(" x ".split(maxsplit=0))
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 122 | print(" x ".split(maxsplit=0))
123 |-print(" x ".split(sep=None, maxsplit=0))
123 |+print(["x "])
124 124 | print("".rsplit(maxsplit=0))
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 126 | print(" ".rsplit(maxsplit=0))
SIM905.py:124:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
122 | print(" x ".split(maxsplit=0))
123 | print(" x ".split(sep=None, maxsplit=0))
124 | print("".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^ SIM905
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
121 121 | print(" x ".split(sep=None, maxsplit=0))
122 122 | print(" x ".split(maxsplit=0))
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 |-print("".rsplit(maxsplit=0))
124 |+print([])
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 126 | print(" ".rsplit(maxsplit=0))
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
SIM905.py:125:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
123 | print(" x ".split(sep=None, maxsplit=0))
124 | print("".rsplit(maxsplit=0))
125 | print("".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
122 122 | print(" x ".split(maxsplit=0))
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 124 | print("".rsplit(maxsplit=0))
125 |-print("".rsplit(sep=None, maxsplit=0))
125 |+print([])
126 126 | print(" ".rsplit(maxsplit=0))
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 128 | print(" x ".rsplit(maxsplit=0))
SIM905.py:126:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
124 | print("".rsplit(maxsplit=0))
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^ SIM905
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
123 123 | print(" x ".split(sep=None, maxsplit=0))
124 124 | print("".rsplit(maxsplit=0))
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 |-print(" ".rsplit(maxsplit=0))
126 |+print([])
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 128 | print(" x ".rsplit(maxsplit=0))
129 129 | print(" x ".rsplit(maxsplit=0))
SIM905.py:127:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
124 124 | print("".rsplit(maxsplit=0))
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 126 | print(" ".rsplit(maxsplit=0))
127 |-print(" ".rsplit(sep=None, maxsplit=0))
127 |+print([])
128 128 | print(" x ".rsplit(maxsplit=0))
129 129 | print(" x ".rsplit(maxsplit=0))
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905.py:128:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
125 125 | print("".rsplit(sep=None, maxsplit=0))
126 126 | print(" ".rsplit(maxsplit=0))
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 |-print(" x ".rsplit(maxsplit=0))
128 |+print([" x"])
129 129 | print(" x ".rsplit(maxsplit=0))
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 131 | print(" x ".rsplit(maxsplit=0))
SIM905.py:129:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
|
= help: Replace with list literal
Safe fix
126 126 | print(" ".rsplit(maxsplit=0))
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 128 | print(" x ".rsplit(maxsplit=0))
129 |-print(" x ".rsplit(maxsplit=0))
129 |+print([" x"])
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 131 | print(" x ".rsplit(maxsplit=0))
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905.py:130:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
127 127 | print(" ".rsplit(sep=None, maxsplit=0))
128 128 | print(" x ".rsplit(maxsplit=0))
129 129 | print(" x ".rsplit(maxsplit=0))
130 |-print(" x ".rsplit(sep=None, maxsplit=0))
130 |+print([" x"])
131 131 | print(" x ".rsplit(maxsplit=0))
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905.py:131:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
= help: Replace with list literal
Safe fix
128 128 | print(" x ".rsplit(maxsplit=0))
129 129 | print(" x ".rsplit(maxsplit=0))
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 |-print(" x ".rsplit(maxsplit=0))
131 |+print([" x"])
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
|
= help: Replace with list literal
Safe fix
129 129 | print(" x ".rsplit(maxsplit=0))
130 130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 131 | print(" x ".rsplit(maxsplit=0))
132 |-print(" x ".rsplit(sep=None, maxsplit=0))
132 |+print([" x"])

View File

@@ -26,109 +26,41 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
// PTH100
["os", "path", "abspath"] => OsPathAbspath.into(),
// PTH101
["os", "chmod"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod)
// ```text
// 0 1 2 3
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
// ```
if call
.arguments
.find_argument_value("path", 0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|| is_argument_non_default(&call.arguments, "dir_fd", 2)
{
return;
}
OsChmod.into()
}
["os", "chmod"] => OsChmod.into(),
// PTH102
["os", "makedirs"] => OsMakedirs.into(),
// PTH103
["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()
}
["os", "mkdir"] => OsMkdir.into(),
// PTH104
["os", "rename"] => {
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
// set to non-default values.
// are set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename)
// ```text
// 0 1 2 3
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
// ```
if is_argument_non_default(&call.arguments, "src_dir_fd", 2)
|| is_argument_non_default(&call.arguments, "dst_dir_fd", 3)
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())
{
return;
}
OsRename.into()
}
// PTH105
["os", "replace"] => {
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
// set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace)
// ```text
// 0 1 2 3
// os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
// ```
if is_argument_non_default(&call.arguments, "src_dir_fd", 2)
|| is_argument_non_default(&call.arguments, "dst_dir_fd", 3)
{
return;
}
OsReplace.into()
}
["os", "replace"] => OsReplace.into(),
// PTH106
["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()
}
["os", "rmdir"] => OsRmdir.into(),
// PTH107
["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()
}
["os", "remove"] => OsRemove.into(),
// PTH108
["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()
}
["os", "unlink"] => OsUnlink.into(),
// PTH109
["os", "getcwd"] => OsGetcwd.into(),
["os", "getcwdb"] => OsGetcwd.into(),
@@ -144,17 +76,10 @@ 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_argument_value("path", 0)
.find_positional(0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|| is_argument_non_default(&call.arguments, "dir_fd", 1)
{
return;
}
@@ -223,10 +148,13 @@ 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("file", 0)
.find_argument_value("opener", 7)
.is_some_and(|expr| !expr.is_none_literal_expr())
|| call
.arguments
.find_positional(0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
{
return;
@@ -236,53 +164,17 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
// PTH124
["py", "path", "local"] => PyPath.into(),
// PTH207
["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()
["glob", "glob"] => Glob {
function: "glob".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(),
["glob", "iglob"] => Glob {
function: "iglob".to_string(),
}
.into(),
// PTH115
// Python 3.9+
["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()
}
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => OsReadlink.into(),
// PTH208
["os", "listdir"] => {
if call
@@ -332,10 +224,3 @@ 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())
}

View File

@@ -1,5 +1,6 @@
---
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`
|
@@ -25,6 +26,4 @@ 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
|

View File

@@ -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 = [] # comment should be protected
147 | tmp = 1; result = [] # commment 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 # comment should be protected
154 | result = []; tmp = 1 # commment should be protected
155 | for i in range(10):
156 | result.append(i + 1) # PERF401
| ^^^^^^^^^^^^^^^^^^^^ PERF401

View File

@@ -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 = [] # comment should be protected
147 | tmp = 1; result = [] # commment 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 = [] # comment should be protected
147 |- tmp = 1; result = [] # commment should be protected
148 |- for i in range(10):
149 |- result.append(i + 1) # PERF401
147 |+ tmp = 1 # comment should be protected
147 |+ tmp = 1 # commment 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 # comment should be protected
154 | result = []; tmp = 1 # commment 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 # comment should be protected
154 |- result = []; tmp = 1 # commment should be protected
155 |- for i in range(10):
156 |- result.append(i + 1) # PERF401
154 |+ tmp = 1 # comment should be protected
154 |+ tmp = 1 # commment should be protected
155 |+ result = [i + 1 for i in range(10)] # PERF401
157 156 |
158 157 |

View File

@@ -38,11 +38,6 @@ use crate::checkers::ast::Checker;
/// nums.add(num + 5)
/// ```
///
/// ## Fix safety
/// This fix is always unsafe because it changes the programs 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)]

View File

@@ -21,7 +21,6 @@ pub(crate) enum MinMax {
/// readability.
///
/// ## Example
///
/// ```python
/// minimum = min(1, 2, min(3, 4, 5))
/// maximum = max(1, 2, max(3, 4, 5))
@@ -29,26 +28,12 @@ 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)

View File

@@ -28,17 +28,6 @@ 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 platforms 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")

View File

@@ -12,11 +12,6 @@ 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

View File

@@ -64,7 +64,7 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool {
[
"" | "builtins",
"EnvironmentError" | "IOError" | "WindowsError"
] | ["mmap" | "resource" | "select" | "socket" | "os", "error"]
] | ["mmap" | "select" | "socket" | "os", "error"]
)
})
}

View File

@@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
UP024_2.py:10:7: UP024 [*] Replace aliased errors with `OSError`
|
8 | # Testing the modules
9 | import socket, mmap, select, resource
9 | import socket, mmap, select
10 | raise socket.error
| ^^^^^^^^^^^^ UP024
11 | raise mmap.error
@@ -15,33 +15,32 @@ UP024_2.py:10:7: UP024 [*] Replace aliased errors with `OSError`
Safe fix
7 7 |
8 8 | # Testing the modules
9 9 | import socket, mmap, select, resource
9 9 | import socket, mmap, select
10 |-raise socket.error
10 |+raise OSError
11 11 | raise mmap.error
12 12 | raise select.error
13 13 | raise resource.error
13 13 |
UP024_2.py:11:7: UP024 [*] Replace aliased errors with `OSError`
|
9 | import socket, mmap, select, resource
9 | import socket, mmap, select
10 | raise socket.error
11 | raise mmap.error
| ^^^^^^^^^^ UP024
12 | raise select.error
13 | raise resource.error
|
= help: Replace `mmap.error` with builtin `OSError`
Safe fix
8 8 | # Testing the modules
9 9 | import socket, mmap, select, resource
9 9 | import socket, mmap, select
10 10 | raise socket.error
11 |-raise mmap.error
11 |+raise OSError
12 12 | raise select.error
13 13 | raise resource.error
14 14 |
13 13 |
14 14 | raise socket.error()
UP024_2.py:12:7: UP024 [*] Replace aliased errors with `OSError`
|
@@ -49,416 +48,355 @@ UP024_2.py:12:7: UP024 [*] Replace aliased errors with `OSError`
11 | raise mmap.error
12 | raise select.error
| ^^^^^^^^^^^^ UP024
13 | raise resource.error
13 |
14 | raise socket.error()
|
= help: Replace `select.error` with builtin `OSError`
Safe fix
9 9 | import socket, mmap, select, resource
9 9 | import socket, mmap, select
10 10 | raise socket.error
11 11 | raise mmap.error
12 |-raise select.error
12 |+raise OSError
13 13 | raise resource.error
14 14 |
15 15 | raise socket.error()
13 13 |
14 14 | raise socket.error()
15 15 | raise mmap.error(1)
UP024_2.py:13:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:14:7: UP024 [*] Replace aliased errors with `OSError`
|
11 | raise mmap.error
12 | raise select.error
13 | raise resource.error
| ^^^^^^^^^^^^^^ UP024
14 |
15 | raise socket.error()
|
= help: Replace `resource.error` with builtin `OSError`
Safe fix
10 10 | raise socket.error
11 11 | raise mmap.error
12 12 | raise select.error
13 |-raise resource.error
13 |+raise OSError
14 14 |
15 15 | raise socket.error()
16 16 | raise mmap.error(1)
UP024_2.py:15:7: UP024 [*] Replace aliased errors with `OSError`
|
13 | raise resource.error
14 |
15 | raise socket.error()
13 |
14 | raise socket.error()
| ^^^^^^^^^^^^ UP024
16 | raise mmap.error(1)
17 | raise select.error(1, 2)
15 | raise mmap.error(1)
16 | raise select.error(1, 2)
|
= help: Replace `socket.error` with builtin `OSError`
Safe fix
11 11 | raise mmap.error
12 12 | raise select.error
13 13 | raise resource.error
14 14 |
15 |-raise socket.error()
15 |+raise OSError()
16 16 | raise mmap.error(1)
17 17 | raise select.error(1, 2)
18 18 | raise resource.error(1, "strerror", "filename")
13 13 |
14 |-raise socket.error()
14 |+raise OSError()
15 15 | raise mmap.error(1)
16 16 | raise select.error(1, 2)
17 17 |
UP024_2.py:16:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:15:7: UP024 [*] Replace aliased errors with `OSError`
|
15 | raise socket.error()
16 | raise mmap.error(1)
14 | raise socket.error()
15 | raise mmap.error(1)
| ^^^^^^^^^^ UP024
17 | raise select.error(1, 2)
18 | raise resource.error(1, "strerror", "filename")
16 | raise select.error(1, 2)
|
= help: Replace `mmap.error` with builtin `OSError`
Safe fix
13 13 | raise resource.error
14 14 |
15 15 | raise socket.error()
16 |-raise mmap.error(1)
16 |+raise OSError(1)
17 17 | raise select.error(1, 2)
18 18 | raise resource.error(1, "strerror", "filename")
19 19 |
12 12 | raise select.error
13 13 |
14 14 | raise socket.error()
15 |-raise mmap.error(1)
15 |+raise OSError(1)
16 16 | raise select.error(1, 2)
17 17 |
18 18 | raise socket.error(
UP024_2.py:17:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:16:7: UP024 [*] Replace aliased errors with `OSError`
|
15 | raise socket.error()
16 | raise mmap.error(1)
17 | raise select.error(1, 2)
14 | raise socket.error()
15 | raise mmap.error(1)
16 | raise select.error(1, 2)
| ^^^^^^^^^^^^ UP024
18 | raise resource.error(1, "strerror", "filename")
17 |
18 | raise socket.error(
|
= help: Replace `select.error` with builtin `OSError`
Safe fix
14 14 |
15 15 | raise socket.error()
16 16 | raise mmap.error(1)
17 |-raise select.error(1, 2)
17 |+raise OSError(1, 2)
18 18 | raise resource.error(1, "strerror", "filename")
19 19 |
20 20 | raise socket.error(
13 13 |
14 14 | raise socket.error()
15 15 | raise mmap.error(1)
16 |-raise select.error(1, 2)
16 |+raise OSError(1, 2)
17 17 |
18 18 | raise socket.error(
19 19 | 1,
UP024_2.py:18:7: UP024 [*] Replace aliased errors with `OSError`
|
16 | raise mmap.error(1)
17 | raise select.error(1, 2)
18 | raise resource.error(1, "strerror", "filename")
| ^^^^^^^^^^^^^^ UP024
19 |
20 | raise socket.error(
|
= help: Replace `resource.error` with builtin `OSError`
Safe fix
15 15 | raise socket.error()
16 16 | raise mmap.error(1)
17 17 | raise select.error(1, 2)
18 |-raise resource.error(1, "strerror", "filename")
18 |+raise OSError(1, "strerror", "filename")
19 19 |
20 20 | raise socket.error(
21 21 | 1,
UP024_2.py:20:7: UP024 [*] Replace aliased errors with `OSError`
|
18 | raise resource.error(1, "strerror", "filename")
19 |
20 | raise socket.error(
16 | raise select.error(1, 2)
17 |
18 | raise socket.error(
| ^^^^^^^^^^^^ UP024
21 | 1,
22 | 2,
19 | 1,
20 | 2,
|
= help: Replace `socket.error` with builtin `OSError`
Safe fix
17 17 | raise select.error(1, 2)
18 18 | raise resource.error(1, "strerror", "filename")
19 19 |
20 |-raise socket.error(
20 |+raise OSError(
21 21 | 1,
22 22 | 2,
23 23 | 3,
15 15 | raise mmap.error(1)
16 16 | raise select.error(1, 2)
17 17 |
18 |-raise socket.error(
18 |+raise OSError(
19 19 | 1,
20 20 | 2,
21 21 | 3,
UP024_2.py:27:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:25:7: UP024 [*] Replace aliased errors with `OSError`
|
26 | from mmap import error
27 | raise error
24 | from mmap import error
25 | raise error
| ^^^^^ UP024
28 |
29 | from socket import error
26 |
27 | from socket import error
|
= help: Replace `error` with builtin `OSError`
Safe fix
24 24 | )
25 25 |
26 26 | from mmap import error
27 |-raise error
27 |+raise OSError
28 28 |
29 29 | from socket import error
30 30 | raise error(1)
22 22 | )
23 23 |
24 24 | from mmap import error
25 |-raise error
25 |+raise OSError
26 26 |
27 27 | from socket import error
28 28 | raise error(1)
UP024_2.py:30:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:28:7: UP024 [*] Replace aliased errors with `OSError`
|
29 | from socket import error
30 | raise error(1)
27 | from socket import error
28 | raise error(1)
| ^^^^^ UP024
31 |
32 | from select import error
29 |
30 | from select import error
|
= help: Replace `error` with builtin `OSError`
Safe fix
27 27 | raise error
28 28 |
29 29 | from socket import error
30 |-raise error(1)
30 |+raise OSError(1)
31 31 |
32 32 | from select import error
33 33 | raise error(1, 2)
25 25 | raise error
26 26 |
27 27 | from socket import error
28 |-raise error(1)
28 |+raise OSError(1)
29 29 |
30 30 | from select import error
31 31 | raise error(1, 2)
UP024_2.py:33:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:31:7: UP024 [*] Replace aliased errors with `OSError`
|
32 | from select import error
33 | raise error(1, 2)
30 | from select import error
31 | raise error(1, 2)
| ^^^^^ UP024
34 |
35 | from resource import error
32 |
33 | # Testing the names
|
= help: Replace `error` with builtin `OSError`
Safe fix
30 30 | raise error(1)
31 31 |
32 32 | from select import error
33 |-raise error(1, 2)
33 |+raise OSError(1, 2)
34 34 |
35 35 | from resource import error
36 36 | raise error(1, "strerror", "filename")
28 28 | raise error(1)
29 29 |
30 30 | from select import error
31 |-raise error(1, 2)
31 |+raise OSError(1, 2)
32 32 |
33 33 | # Testing the names
34 34 | raise EnvironmentError
UP024_2.py:34:7: UP024 [*] Replace aliased errors with `OSError`
|
33 | # Testing the names
34 | raise EnvironmentError
| ^^^^^^^^^^^^^^^^ UP024
35 | raise IOError
36 | raise WindowsError
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
31 31 | raise error(1, 2)
32 32 |
33 33 | # Testing the names
34 |-raise EnvironmentError
34 |+raise OSError
35 35 | raise IOError
36 36 | raise WindowsError
37 37 |
UP024_2.py:35:7: UP024 [*] Replace aliased errors with `OSError`
|
33 | # Testing the names
34 | raise EnvironmentError
35 | raise IOError
| ^^^^^^^ UP024
36 | raise WindowsError
|
= help: Replace `IOError` with builtin `OSError`
Safe fix
32 32 |
33 33 | # Testing the names
34 34 | raise EnvironmentError
35 |-raise IOError
35 |+raise OSError
36 36 | raise WindowsError
37 37 |
38 38 | raise EnvironmentError()
UP024_2.py:36:7: UP024 [*] Replace aliased errors with `OSError`
|
35 | from resource import error
36 | raise error(1, "strerror", "filename")
| ^^^^^ UP024
34 | raise EnvironmentError
35 | raise IOError
36 | raise WindowsError
| ^^^^^^^^^^^^ UP024
37 |
38 | # Testing the names
38 | raise EnvironmentError()
|
= help: Replace `error` with builtin `OSError`
= help: Replace `WindowsError` with builtin `OSError`
Safe fix
33 33 | raise error(1, 2)
34 34 |
35 35 | from resource import error
36 |-raise error(1, "strerror", "filename")
36 |+raise OSError(1, "strerror", "filename")
33 33 | # Testing the names
34 34 | raise EnvironmentError
35 35 | raise IOError
36 |-raise WindowsError
36 |+raise OSError
37 37 |
38 38 | # Testing the names
39 39 | raise EnvironmentError
38 38 | raise EnvironmentError()
39 39 | raise IOError(1)
UP024_2.py:38:7: UP024 [*] Replace aliased errors with `OSError`
|
36 | raise WindowsError
37 |
38 | raise EnvironmentError()
| ^^^^^^^^^^^^^^^^ UP024
39 | raise IOError(1)
40 | raise WindowsError(1, 2)
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
35 35 | raise IOError
36 36 | raise WindowsError
37 37 |
38 |-raise EnvironmentError()
38 |+raise OSError()
39 39 | raise IOError(1)
40 40 | raise WindowsError(1, 2)
41 41 |
UP024_2.py:39:7: UP024 [*] Replace aliased errors with `OSError`
|
38 | # Testing the names
39 | raise EnvironmentError
| ^^^^^^^^^^^^^^^^ UP024
40 | raise IOError
41 | raise WindowsError
38 | raise EnvironmentError()
39 | raise IOError(1)
| ^^^^^^^ UP024
40 | raise WindowsError(1, 2)
|
= help: Replace `EnvironmentError` with builtin `OSError`
= help: Replace `IOError` with builtin `OSError`
Safe fix
36 36 | raise error(1, "strerror", "filename")
36 36 | raise WindowsError
37 37 |
38 38 | # Testing the names
39 |-raise EnvironmentError
39 |+raise OSError
40 40 | raise IOError
41 41 | raise WindowsError
42 42 |
38 38 | raise EnvironmentError()
39 |-raise IOError(1)
39 |+raise OSError(1)
40 40 | raise WindowsError(1, 2)
41 41 |
42 42 | raise EnvironmentError(
UP024_2.py:40:7: UP024 [*] Replace aliased errors with `OSError`
|
38 | # Testing the names
39 | raise EnvironmentError
40 | raise IOError
| ^^^^^^^ UP024
41 | raise WindowsError
38 | raise EnvironmentError()
39 | raise IOError(1)
40 | raise WindowsError(1, 2)
| ^^^^^^^^^^^^ UP024
41 |
42 | raise EnvironmentError(
|
= help: Replace `IOError` with builtin `OSError`
= help: Replace `WindowsError` with builtin `OSError`
Safe fix
37 37 |
38 38 | # Testing the names
39 39 | raise EnvironmentError
40 |-raise IOError
40 |+raise OSError
41 41 | raise WindowsError
42 42 |
43 43 | raise EnvironmentError()
38 38 | raise EnvironmentError()
39 39 | raise IOError(1)
40 |-raise WindowsError(1, 2)
40 |+raise OSError(1, 2)
41 41 |
42 42 | raise EnvironmentError(
43 43 | 1,
UP024_2.py:41:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:42:7: UP024 [*] Replace aliased errors with `OSError`
|
39 | raise EnvironmentError
40 | raise IOError
41 | raise WindowsError
| ^^^^^^^^^^^^ UP024
42 |
43 | raise EnvironmentError()
|
= help: Replace `WindowsError` with builtin `OSError`
Safe fix
38 38 | # Testing the names
39 39 | raise EnvironmentError
40 40 | raise IOError
41 |-raise WindowsError
41 |+raise OSError
42 42 |
43 43 | raise EnvironmentError()
44 44 | raise IOError(1)
UP024_2.py:43:7: UP024 [*] Replace aliased errors with `OSError`
|
41 | raise WindowsError
42 |
43 | raise EnvironmentError()
40 | raise WindowsError(1, 2)
41 |
42 | raise EnvironmentError(
| ^^^^^^^^^^^^^^^^ UP024
44 | raise IOError(1)
45 | raise WindowsError(1, 2)
43 | 1,
44 | 2,
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
40 40 | raise IOError
41 41 | raise WindowsError
42 42 |
43 |-raise EnvironmentError()
43 |+raise OSError()
44 44 | raise IOError(1)
45 45 | raise WindowsError(1, 2)
46 46 |
39 39 | raise IOError(1)
40 40 | raise WindowsError(1, 2)
41 41 |
42 |-raise EnvironmentError(
42 |+raise OSError(
43 43 | 1,
44 44 | 2,
45 45 | 3,
UP024_2.py:44:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:48:7: UP024 [*] Replace aliased errors with `OSError`
|
43 | raise EnvironmentError()
44 | raise IOError(1)
| ^^^^^^^ UP024
45 | raise WindowsError(1, 2)
|
= help: Replace `IOError` with builtin `OSError`
Safe fix
41 41 | raise WindowsError
42 42 |
43 43 | raise EnvironmentError()
44 |-raise IOError(1)
44 |+raise OSError(1)
45 45 | raise WindowsError(1, 2)
46 46 |
47 47 | raise EnvironmentError(
UP024_2.py:45:7: UP024 [*] Replace aliased errors with `OSError`
|
43 | raise EnvironmentError()
44 | raise IOError(1)
45 | raise WindowsError(1, 2)
46 | )
47 |
48 | raise WindowsError
| ^^^^^^^^^^^^ UP024
46 |
47 | raise EnvironmentError(
49 | raise EnvironmentError(1)
50 | raise IOError(1, 2)
|
= help: Replace `WindowsError` with builtin `OSError`
Safe fix
42 42 |
43 43 | raise EnvironmentError()
44 44 | raise IOError(1)
45 |-raise WindowsError(1, 2)
45 |+raise OSError(1, 2)
46 46 |
47 47 | raise EnvironmentError(
48 48 | 1,
45 45 | 3,
46 46 | )
47 47 |
48 |-raise WindowsError
48 |+raise OSError
49 49 | raise EnvironmentError(1)
50 50 | raise IOError(1, 2)
UP024_2.py:47:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:49:7: UP024 [*] Replace aliased errors with `OSError`
|
45 | raise WindowsError(1, 2)
46 |
47 | raise EnvironmentError(
48 | raise WindowsError
49 | raise EnvironmentError(1)
| ^^^^^^^^^^^^^^^^ UP024
48 | 1,
49 | 2,
50 | raise IOError(1, 2)
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
44 44 | raise IOError(1)
45 45 | raise WindowsError(1, 2)
46 46 |
47 |-raise EnvironmentError(
47 |+raise OSError(
48 48 | 1,
49 49 | 2,
50 50 | 3,
46 46 | )
47 47 |
48 48 | raise WindowsError
49 |-raise EnvironmentError(1)
49 |+raise OSError(1)
50 50 | raise IOError(1, 2)
UP024_2.py:53:7: UP024 [*] Replace aliased errors with `OSError`
UP024_2.py:50:7: UP024 [*] Replace aliased errors with `OSError`
|
51 | )
52 |
53 | raise WindowsError
| ^^^^^^^^^^^^ UP024
54 | raise EnvironmentError(1)
55 | raise IOError(1, 2)
|
= help: Replace `WindowsError` with builtin `OSError`
Safe fix
50 50 | 3,
51 51 | )
52 52 |
53 |-raise WindowsError
53 |+raise OSError
54 54 | raise EnvironmentError(1)
55 55 | raise IOError(1, 2)
UP024_2.py:54:7: UP024 [*] Replace aliased errors with `OSError`
|
53 | raise WindowsError
54 | raise EnvironmentError(1)
| ^^^^^^^^^^^^^^^^ UP024
55 | raise IOError(1, 2)
|
= help: Replace `EnvironmentError` with builtin `OSError`
Safe fix
51 51 | )
52 52 |
53 53 | raise WindowsError
54 |-raise EnvironmentError(1)
54 |+raise OSError(1)
55 55 | raise IOError(1, 2)
UP024_2.py:55:7: UP024 [*] Replace aliased errors with `OSError`
|
53 | raise WindowsError
54 | raise EnvironmentError(1)
55 | raise IOError(1, 2)
48 | raise WindowsError
49 | raise EnvironmentError(1)
50 | raise IOError(1, 2)
| ^^^^^^^ UP024
|
= help: Replace `IOError` with builtin `OSError`
Safe fix
52 52 |
53 53 | raise WindowsError
54 54 | raise EnvironmentError(1)
55 |-raise IOError(1, 2)
55 |+raise OSError(1, 2)
47 47 |
48 48 | raise WindowsError
49 49 | raise EnvironmentError(1)
50 |-raise IOError(1, 2)
50 |+raise OSError(1, 2)

View File

@@ -89,15 +89,4 @@ 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(())
}
}

View File

@@ -1,7 +1,6 @@
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr, ExprCall, Number, PythonVersion, UnaryOp};
use ruff_source_file::find_newline;
use ruff_python_ast::{self as ast, Expr, ExprCall, Number};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -25,11 +24,6 @@ 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>,
@@ -127,23 +121,20 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs
return;
}
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 {
arg
};
// Generate a replacement, if possible.
let replacement = if matches!(
arg,
Expr::NumberLiteral(_) | Expr::Name(_) | Expr::Attribute(_)
) {
let inner_source = checker.locator().slice(arg);
let applicability = if matches!(maybe_number, Expr::NumberLiteral(_)) {
Applicability::Safe
} else {
Applicability::DisplayOnly
};
let quote = checker.stylist().quote();
let shorthand = base.shorthand();
let replacement = try_create_replacement(checker, arg, base);
Some(format!("f{quote}{{{inner_source}:{shorthand}}}{quote}"))
} else {
None
};
let mut diagnostic = Diagnostic::new(
FStringNumberFormat {
@@ -154,54 +145,15 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs
);
if let Some(replacement) = replacement {
let edit = Edit::range_replacement(replacement, subscript.range());
diagnostic.set_fix(Fix::applicable_edit(edit, applicability));
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
replacement,
subscript.range(),
)));
}
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,

View File

@@ -1,288 +1,144 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB116.py:9:7: FURB116 Replace `oct` call with `f"{num:o}"`
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}"`
|
7 | return num
8 |
9 | print(oct(num)[2:]) # FURB116
6 | print(oct(num)[2:]) # FURB116
7 | print(hex(num)[2:]) # FURB116
8 | print(bin(num)[2:]) # FURB116
| ^^^^^^^^^^^^ 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
9 |
10 | print(oct(1337)[2:]) # FURB116
|
= help: Replace with `f"{num:b}"`
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
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
FURB116.py:13:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
FURB116.py:10:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
|
11 | print(bin(num)[2:]) # FURB116
12 |
13 | print(oct(1337)[2:]) # FURB116
8 | print(bin(num)[2:]) # FURB116
9 |
10 | print(oct(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
11 | print(hex(1337)[2:]) # FURB116
12 | print(bin(1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:o}"`
Safe fix
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
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 |
FURB116.py:14:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
FURB116.py:11:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
|
13 | print(oct(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
10 | print(oct(1337)[2:]) # FURB116
11 | print(hex(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
15 | print(bin(1337)[2:]) # FURB116
16 | print(bin(+1337)[2:]) # FURB116
12 | print(bin(1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:x}"`
Safe fix
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 |
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)
FURB116.py:15:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
FURB116.py:12:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
|
13 | print(oct(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
10 | print(oct(1337)[2:]) # FURB116
11 | print(hex(1337)[2:]) # FURB116
12 | print(bin(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
16 | print(bin(+1337)[2:]) # FURB116
13 |
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
= help: Replace with `f"{1337:b}"`
Safe fix
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)
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)
FURB116.py:16:7: FURB116 [*] Replace `bin` call with `f"{+1337:b}"`
FURB116.py:14:7: FURB116 Replace `bin` call with f-string
|
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)
12 | print(bin(1337)[2:]) # FURB116
13 |
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
| ^^^^^^^^^^^^^^^^^^^^^ FURB116
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
= help: Replace with f-string
FURB116.py:19:7: FURB116 Replace `bin` call with f-string
FURB116.py:15:7: FURB116 Replace `bin` call with f-string
|
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
| ^^^^^^^^^^^^^^^^^^^^^^ FURB116
20 |
21 | ## invalid
16 |
17 | ## 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}")

View File

@@ -1,256 +0,0 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB116.py:9:7: FURB116 Replace `oct` call with `f"{num:o}"`
|
7 | return num
8 |
9 | print(oct(num)[2:]) # FURB116
| ^^^^^^^^^^^^ 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}"`
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:13:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
|
11 | print(bin(num)[2:]) # FURB116
12 |
13 | print(oct(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:o}"`
Safe fix
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:14:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
|
13 | print(oct(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
15 | print(bin(1337)[2:]) # FURB116
16 | print(bin(+1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:x}"`
Safe fix
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:15:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
|
13 | print(oct(1337)[2:]) # FURB116
14 | print(hex(1337)[2:]) # FURB116
15 | print(bin(1337)[2:]) # FURB116
| ^^^^^^^^^^^^^ FURB116
16 | print(bin(+1337)[2:]) # FURB116
|
= help: Replace with `f"{1337:b}"`
Safe fix
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:16:7: FURB116 [*] Replace `bin` call with `f"{+1337:b}"`
|
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
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
= help: Replace with f-string
FURB116.py:19:7: FURB116 Replace `bin` call with f-string
|
18 | print(bin(return_num())[2:]) # FURB116 (no autofix)
19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
| ^^^^^^^^^^^^^^^^^^^^^^ FURB116
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-string
|
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-string
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-string
|
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-string
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
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}")

View File

@@ -1,7 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -49,14 +48,6 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare)
};
let semantic = checker.semantic();
if is_empty(right, semantic) {
checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range()));
}
}
fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool {
let set_methods = ["set", "frozenset"];
let collection_methods = [
"list",
"tuple",
@@ -68,7 +59,7 @@ fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool {
"str",
];
match expr {
let is_empty_collection = match right {
Expr::List(ast::ExprList { elts, .. }) => elts.is_empty(),
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.is_empty(),
Expr::Set(ast::ExprSet { elts, .. }) => elts.is_empty(),
@@ -84,19 +75,15 @@ fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool {
arguments,
range: _,
}) => {
if arguments.is_empty() {
collection_methods
arguments.is_empty()
&& collection_methods
.iter()
.any(|s| semantic.match_builtin_expr(func, s))
} else if let Some(arg) = arguments.find_positional(0) {
set_methods
.iter()
.any(|s| semantic.match_builtin_expr(func, s))
&& is_empty(arg, semantic)
} else {
false
}
}
_ => false,
};
if is_empty_collection {
checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range()));
}
}

View File

@@ -61,12 +61,6 @@ use super::helpers::{dataclass_kind, DataclassKind};
/// foo = Foo() # Prints '1 2'.
/// ```
///
/// ## Fix safety
///
/// This fix is always marked as unsafe because, although switching to `InitVar` is usually correct,
/// it is incorrect when the parameter is not intended to be part of the public API or when the value
/// is meant to be shared across all instances.
///
/// ## References
/// - [Python documentation: Post-init processing](https://docs.python.org/3/library/dataclasses.html#post-init-processing)
/// - [Python documentation: Init-only variables](https://docs.python.org/3/library/dataclasses.html#init-only-variables)

View File

@@ -29,14 +29,6 @@ use crate::{checkers::ast::Checker, importer::ImportRequest};
/// pairwise(letters) # ("A", "B"), ("B", "C"), ("C", "D")
/// ```
///
/// ## Fix safety
///
/// The fix is always marked unsafe because it assumes that slicing an object
/// (e.g., `obj[1:]`) produces a value with the same type and iteration behavior
/// as the original object, which is not guaranteed for user-defined types that
/// override `__getitem__` without properly handling slices. Moreover, the fix
/// could delete comments.
///
/// ## References
/// - [Python documentation: `itertools.pairwise`](https://docs.python.org/3/library/itertools.html#itertools.pairwise)
#[derive(ViolationMetadata)]

View File

@@ -187,7 +187,6 @@ RUF060.py:20:1: RUF060 Unnecessary membership test on empty collection
20 | b"a" in bytes()
| ^^^^^^^^^^^^^^^ RUF060
21 | 1 in frozenset()
22 | 1 in set(set())
|
RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection
@@ -196,35 +195,6 @@ RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection
20 | b"a" in bytes()
21 | 1 in frozenset()
| ^^^^^^^^^^^^^^^^ RUF060
22 | 1 in set(set())
23 | 2 in frozenset([])
|
RUF060.py:22:1: RUF060 Unnecessary membership test on empty collection
|
20 | b"a" in bytes()
21 | 1 in frozenset()
22 | 1 in set(set())
| ^^^^^^^^^^^^^^^ RUF060
23 | 2 in frozenset([])
24 | '' in set("")
|
RUF060.py:23:1: RUF060 Unnecessary membership test on empty collection
|
21 | 1 in frozenset()
22 | 1 in set(set())
23 | 2 in frozenset([])
| ^^^^^^^^^^^^^^^^^^ RUF060
24 | '' in set("")
|
RUF060.py:24:1: RUF060 Unnecessary membership test on empty collection
|
22 | 1 in set(set())
23 | 2 in frozenset([])
24 | '' in set("")
| ^^^^^^^^^^^^^ RUF060
25 |
26 | # OK
22 |
23 | # OK
|

View File

@@ -1,8 +0,0 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:2:22: SyntaxError: named expression cannot be used within a generic definition
|
2 | def f[T](x: int) -> (y := 3): return x
| ^^^^^^
|

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