Compare commits
45 Commits
0.8.2
...
micha/erro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b2544993 | ||
|
|
fe78d50560 | ||
|
|
b39def2915 | ||
|
|
cf260aef2b | ||
|
|
59145098d6 | ||
|
|
3d9ac535e9 | ||
|
|
8df4983057 | ||
|
|
56a631a868 | ||
|
|
9d641fa714 | ||
|
|
b56b3c813c | ||
|
|
c59d54370b | ||
|
|
dfaf2de81d | ||
|
|
d53e5cd25a | ||
|
|
ed6de39725 | ||
|
|
f4a7da7e93 | ||
|
|
8bf04988fb | ||
|
|
745a4b425e | ||
|
|
dfab134bb6 | ||
|
|
58e7db89a1 | ||
|
|
8c4b22964e | ||
|
|
ecd948a083 | ||
|
|
9b8ceb9a2e | ||
|
|
8d9e408dbb | ||
|
|
85402097fc | ||
|
|
269e47be96 | ||
|
|
d34013425f | ||
|
|
2c13e6513d | ||
|
|
8fdd88013d | ||
|
|
3017b3b687 | ||
|
|
2119dcab6f | ||
|
|
3ea14d7a74 | ||
|
|
4fdd4ddfaa | ||
|
|
6b9f3d7d7c | ||
|
|
4cb8392523 | ||
|
|
9ee438b02f | ||
|
|
89368a62a8 | ||
|
|
1559c73fcd | ||
|
|
39623f8d40 | ||
|
|
918358aaa6 | ||
|
|
b01a651e69 | ||
|
|
56afb12ae7 | ||
|
|
b42e528555 | ||
|
|
5aab57b3e9 | ||
|
|
40b0b67dd9 | ||
|
|
1bd8fbb6e8 |
8
.github/workflows/build-binaries.yml
vendored
8
.github/workflows/build-binaries.yml
vendored
@@ -40,6 +40,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -68,6 +69,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -109,6 +111,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -164,6 +167,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -216,6 +220,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -290,6 +295,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -354,6 +360,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -419,6 +426,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
1
.github/workflows/build-docker.yml
vendored
1
.github/workflows/build-docker.yml
vendored
@@ -36,6 +36,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
||||
38
.github/workflows/ci.yaml
vendored
38
.github/workflows/ci.yaml
vendored
@@ -38,6 +38,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: tj-actions/changed-files@v45
|
||||
id: changed
|
||||
@@ -99,6 +100,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
- run: cargo fmt --all --check
|
||||
@@ -111,6 +114,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: |
|
||||
rustup component add clippy
|
||||
@@ -129,6 +134,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -173,6 +180,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -200,6 +209,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
@@ -224,6 +235,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -251,6 +264,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -267,6 +282,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: SebRollen/toml-action@v1.2.0
|
||||
id: msrv
|
||||
with:
|
||||
@@ -299,6 +316,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -325,6 +344,8 @@ jobs:
|
||||
FORCE_COLOR: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Download Ruff binary to test
|
||||
@@ -355,6 +376,8 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -379,6 +402,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -489,6 +514,8 @@ jobs:
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
@@ -499,6 +526,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -524,6 +553,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -555,6 +586,8 @@ jobs:
|
||||
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
@@ -595,6 +628,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Cache rust"
|
||||
@@ -622,6 +657,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
name: "Download ruff-lsp source"
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
@@ -657,6 +693,8 @@ jobs:
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -32,6 +32,8 @@ jobs:
|
||||
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
1
.github/workflows/publish-docs.yml
vendored
1
.github/workflows/publish-docs.yml
vendored
@@ -26,6 +26,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
2
.github/workflows/publish-playground.yml
vendored
2
.github/workflows/publish-playground.yml
vendored
@@ -25,6 +25,8 @@ jobs:
|
||||
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/publish-wasm.yml
vendored
2
.github/workflows/publish-wasm.yml
vendored
@@ -30,6 +30,8 @@ jobs:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
|
||||
2
.github/workflows/sync_typeshed.yaml
vendored
2
.github/workflows/sync_typeshed.yaml
vendored
@@ -25,11 +25,13 @@ jobs:
|
||||
name: Checkout Ruff
|
||||
with:
|
||||
path: ruff
|
||||
persist-credentials: true
|
||||
- uses: actions/checkout@v4
|
||||
name: Checkout typeshed
|
||||
with:
|
||||
repository: python/typeshed
|
||||
path: typeshed
|
||||
persist-credentials: true
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.28.1
|
||||
rev: v1.28.2
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -73,7 +73,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.1
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -83,10 +83,25 @@ repos:
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.4.1
|
||||
rev: v3.4.2
|
||||
hooks:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v0.8.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
# `release.yml` is autogenerated by `dist`; security issues need to be fixed there
|
||||
# (https://opensource.axo.dev/cargo-dist/)
|
||||
exclude: .github/workflows/release.yml
|
||||
# We could consider enabling the low-severity warnings, but they're noisy
|
||||
args: [--min-severity=medium]
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.30.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
ci:
|
||||
skip: [cargo-fmt, dev-generate-all]
|
||||
|
||||
83
Cargo.lock
generated
83
Cargo.lock
generated
@@ -117,9 +117,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.93"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
||||
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
@@ -276,13 +276,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.95"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
|
||||
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -353,9 +353,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.21"
|
||||
version = "4.5.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
|
||||
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -363,9 +363,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.21"
|
||||
version = "4.5.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
|
||||
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -418,9 +418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.0"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "clearscreen"
|
||||
@@ -758,18 +758,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dir-test"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b12781621d53fd9087021f5a338df5c57c04f84a6231c1f4726f45e2e333470b"
|
||||
checksum = "62c013fe825864f3e4593f36426c1fa7a74f5603f13ca8d1af7a990c1cd94a79"
|
||||
dependencies = [
|
||||
"dir-test-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dir-test-macros"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1340852f50b2285d01a7f598cc5d08b572669c3e09e614925175cc3c26787b91"
|
||||
checksum = "d42f54d7b4a6bc2400fe5b338e35d1a335787585375322f49c5d5fe7b243da7e"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
@@ -1944,10 +1944,11 @@ checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
|
||||
|
||||
[[package]]
|
||||
name = "pep440_rs"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0922a442c78611fa8c5ed6065d2d898a820cf12fa90604217fdb2d01675efec7"
|
||||
checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"serde",
|
||||
"unicode-width 0.2.0",
|
||||
"unscanny",
|
||||
@@ -2160,7 +2161,7 @@ dependencies = [
|
||||
"newtype-uuid",
|
||||
"quick-xml",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2299,6 +2300,7 @@ dependencies = [
|
||||
"red_knot_vendored",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_literal",
|
||||
"ruff_python_parser",
|
||||
@@ -2312,7 +2314,7 @@ dependencies = [
|
||||
"static_assertions",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -2358,7 +2360,9 @@ dependencies = [
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2407,7 +2411,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa",
|
||||
"serde",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2560,7 +2564,7 @@ dependencies = [
|
||||
"strum",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -2630,7 +2634,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
@@ -2782,7 +2786,7 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"test-case",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
@@ -2816,7 +2820,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"test-case",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2888,7 +2892,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3021,7 +3025,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.6",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -3409,6 +3413,12 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.6.0"
|
||||
@@ -3613,11 +3623,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.3"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
|
||||
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.3",
|
||||
"thiserror-impl 2.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3633,9 +3643,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.3"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
|
||||
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3787,9 +3797,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-indicatif"
|
||||
version = "0.3.6"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "069580424efe11d97c3fef4197fa98c004fa26672cc71ad8770d224e23b1951d"
|
||||
checksum = "74ba258e9de86447f75edf6455fded8e5242704c6fccffe7bf8d7fb6daef1180"
|
||||
dependencies = [
|
||||
"indicatif",
|
||||
"tracing",
|
||||
@@ -3961,21 +3971,18 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.11.0"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b30e6f97efe1fa43535ee241ee76967d3ff6ff3953ebb430d8d55c5393029e7b"
|
||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"flate2",
|
||||
"litemap",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -119,6 +119,10 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
|
||||
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
|
||||
|
||||
```shell
|
||||
# With uv.
|
||||
uv add --dev ruff # to add ruff to your project
|
||||
uv tool install ruff # to install ruff globally
|
||||
|
||||
# With pip.
|
||||
pip install ruff
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
|
||||
@@ -12,7 +12,7 @@ Parts of the testcases defined here were adapted from [the specification's examp
|
||||
It can be used anywhere a type is accepted:
|
||||
|
||||
```py
|
||||
from typing import LiteralString
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
x: LiteralString
|
||||
|
||||
@@ -25,7 +25,7 @@ def f():
|
||||
`LiteralString` cannot be used within `Literal`:
|
||||
|
||||
```py
|
||||
from typing import Literal, LiteralString
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
bad_union: Literal["hello", LiteralString] # error: [invalid-literal-parameter]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
|
||||
@@ -36,7 +36,7 @@ bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
|
||||
`LiteralString` cannot be parametrized.
|
||||
|
||||
```py
|
||||
from typing import LiteralString
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
a: LiteralString[str] # error: [invalid-type-parameter]
|
||||
b: LiteralString["foo"] # error: [invalid-type-parameter]
|
||||
@@ -47,7 +47,7 @@ b: LiteralString["foo"] # error: [invalid-type-parameter]
|
||||
Subclassing `LiteralString` leads to a runtime error.
|
||||
|
||||
```py
|
||||
from typing import LiteralString
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
class C(LiteralString): ... # error: [invalid-base]
|
||||
```
|
||||
@@ -57,6 +57,8 @@ class C(LiteralString): ... # error: [invalid-base]
|
||||
### Common operations
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
foo: LiteralString = "foo"
|
||||
reveal_type(foo) # revealed: Literal["foo"]
|
||||
|
||||
@@ -85,6 +87,8 @@ reveal_type(template.format(foo, bar)) # revealed: @Todo(call todo)
|
||||
vice versa.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
@@ -112,6 +116,8 @@ qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
### Narrowing
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
lorem: LiteralString = "lorem" * 1_000_000_000
|
||||
|
||||
reveal_type(lorem) # revealed: LiteralString
|
||||
@@ -125,4 +131,22 @@ if "" < lorem == "ipsum":
|
||||
reveal_type(lorem) # revealed: Literal["ipsum"]
|
||||
```
|
||||
|
||||
## `typing.LiteralString`
|
||||
|
||||
`typing.LiteralString` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import LiteralString
|
||||
|
||||
x: LiteralString = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
```
|
||||
|
||||
[1]: https://typing.readthedocs.io/en/latest/spec/literal.html#literalstring
|
||||
|
||||
@@ -19,12 +19,11 @@ reveal_type(stop())
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import NoReturn, Never, Any
|
||||
from typing_extensions import NoReturn, Never, Any
|
||||
|
||||
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
|
||||
x: Never[int]
|
||||
a1: NoReturn
|
||||
# TODO: Test `Never` is only available in python >= 3.11
|
||||
a2: Never
|
||||
b1: Any
|
||||
b2: int
|
||||
@@ -46,17 +45,20 @@ def f():
|
||||
v6: Never = 1
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
## `typing.Never`
|
||||
|
||||
`typing.Never` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import NoReturn, Never
|
||||
from typing import Never
|
||||
|
||||
x: NoReturn
|
||||
y: Never
|
||||
x: Never
|
||||
|
||||
def f():
|
||||
# revealed: Never
|
||||
reveal_type(x)
|
||||
# revealed: Never
|
||||
reveal_type(y)
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
|
||||
@@ -8,8 +8,8 @@ from typing_extensions import TypeVarTuple
|
||||
Ts = TypeVarTuple("Ts")
|
||||
|
||||
def append_int(*args: *Ts) -> tuple[*Ts, int]:
|
||||
# TODO: should show some representation of the variadic generic type
|
||||
reveal_type(args) # revealed: @Todo(function parameter type)
|
||||
# TODO: tuple[*Ts]
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
return (*args, 1)
|
||||
|
||||
|
||||
@@ -95,37 +95,37 @@ reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
|
||||
## Various string kinds
|
||||
|
||||
```py
|
||||
# error: [annotation-raw-string] "Type expressions cannot use raw string literal"
|
||||
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
|
||||
def f1() -> r"int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-f-string] "Type expressions cannot use f-strings"
|
||||
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
|
||||
def f2() -> f"int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
def f3() -> b"int":
|
||||
return 1
|
||||
|
||||
def f4() -> "int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals"
|
||||
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
|
||||
def f5() -> "in" "t":
|
||||
return 1
|
||||
|
||||
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
def f6() -> "\N{LATIN SMALL LETTER I}nt":
|
||||
return 1
|
||||
|
||||
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
def f7() -> "\x69nt":
|
||||
return 1
|
||||
|
||||
def f8() -> """int""":
|
||||
return 1
|
||||
|
||||
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
def f9() -> "b'int'":
|
||||
return 1
|
||||
|
||||
@@ -208,9 +208,9 @@ i: "{i for i in range(5)}"
|
||||
j: "{i: i for i in range(5)}"
|
||||
k: "(i for i in range(5))"
|
||||
l: "await 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
m: "yield 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
n: "yield from 1"
|
||||
o: "1 < 2"
|
||||
p: "call()"
|
||||
|
||||
@@ -50,7 +50,7 @@ reveal_type(b) # revealed: tuple[int]
|
||||
reveal_type(c) # revealed: tuple[str, int]
|
||||
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
|
||||
# TODO: homogenous tuples, PEP-646 tuples
|
||||
# TODO: homogeneous tuples, PEP-646 tuples
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
@@ -18,43 +18,3 @@ Note: in this particular example, one could argue that the most likely error wou
|
||||
of the `x`/`foo` definitions, and so it could be desirable to infer `Literal[1]` for the type of
|
||||
`x`. On the other hand, there might be a variable `fob` a little higher up in this file, and the
|
||||
actual error might have been just a typo. Inferring `Unknown` thus seems like the safest option.
|
||||
|
||||
## Unbound class variable
|
||||
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
y = x
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Possibly unbound in class and global scope
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
x = "abc"
|
||||
|
||||
class C:
|
||||
if bool_instance():
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Function parameter types
|
||||
|
||||
Within a function scope, the declared type of each parameter is its annotated type (or Unknown if
|
||||
not annotated). The initial inferred type is the union of the declared type with the type of the
|
||||
default value expression (if any). If both are fully static types, this union should simplify to the
|
||||
annotated type (since the default value type must be assignable to the annotated type, and for fully
|
||||
static types this means subtype-of, which simplifies in unions). But if the annotated type is
|
||||
Unknown or another non-fully-static type, the default value type may still be relevant as lower
|
||||
bound.
|
||||
|
||||
The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is
|
||||
a dictionary from strings to its annotated type.
|
||||
|
||||
## Parameter kinds
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, h: Literal[6] = 6, **kwargs: str):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: int
|
||||
reveal_type(c) # revealed: Unknown | Literal[1]
|
||||
reveal_type(d) # revealed: int
|
||||
reveal_type(e) # revealed: Unknown | Literal[3]
|
||||
reveal_type(f) # revealed: Literal[4]
|
||||
reveal_type(g) # revealed: Unknown | Literal[5]
|
||||
reveal_type(h) # revealed: Literal[6]
|
||||
|
||||
# TODO: should be `tuple[object, ...]` (needs generics)
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
# TODO: should be `dict[str, str]` (needs generics)
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
```
|
||||
|
||||
## Unannotated variadic parameters
|
||||
|
||||
...are inferred as tuple of Unknown or dict from string to Unknown.
|
||||
|
||||
```py
|
||||
def g(*args, **kwargs):
|
||||
# TODO: should be `tuple[Unknown, ...]` (needs generics)
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
# TODO: should be `dict[str, Unknown]` (needs generics)
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
```
|
||||
|
||||
## Annotation is present but not a fully static type
|
||||
|
||||
The default value type should be a lower bound on the inferred type.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(x: Any = 1):
|
||||
reveal_type(x) # revealed: Any | Literal[1]
|
||||
```
|
||||
|
||||
## Default value type must be assignable to annotated type
|
||||
|
||||
The default value type must be assignable to the annotated type. If not, we emit a diagnostic, and
|
||||
fall back to inferring the annotated type, ignoring the default value type.
|
||||
|
||||
```py
|
||||
# error: [invalid-parameter-default]
|
||||
def f(x: int = "foo"):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# The check is assignable-to, not subtype-of, so this is fine:
|
||||
from typing import Any
|
||||
|
||||
def g(x: Any = "foo"):
|
||||
reveal_type(x) # revealed: Any | Literal["foo"]
|
||||
```
|
||||
@@ -73,7 +73,7 @@ def f[T]():
|
||||
A typevar with less than two constraints emits a diagnostic:
|
||||
|
||||
```py
|
||||
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
|
||||
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -55,3 +55,24 @@ from b import x
|
||||
|
||||
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
|
||||
```
|
||||
|
||||
## Import cycle
|
||||
|
||||
```py path=a.py
|
||||
class A: ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]]
|
||||
import b
|
||||
|
||||
class C(b.B): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
from a import A
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Invalid syntax
|
||||
|
||||
## Missing module name
|
||||
|
||||
```py
|
||||
from import bar # error: [invalid-syntax]
|
||||
|
||||
reveal_type(bar) # revealed: Unknown
|
||||
```
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
Test cases to ensure that red knot does not panic if there are syntax errors in the source code.
|
||||
|
||||
The parser cannot recover from certain syntax errors completely which is why the number of syntax
|
||||
errors could be more than expected in the following examples. For instance, if there's a keyword
|
||||
(like `for`) in the middle of another statement (like function definition), then it's more likely
|
||||
that the rest of the tokens are going to be part of the `for` statement and not the function
|
||||
definition. But, it's not necessary that the remaining tokens are valid in the context of a `for`
|
||||
statement.
|
||||
|
||||
## Keyword as identifiers
|
||||
|
||||
When keywords are used as identifiers, the parser recovers from this syntax error by emitting an
|
||||
@@ -9,35 +16,57 @@ error and including the text value of the keyword to create the `Identifier` nod
|
||||
|
||||
### Name expression
|
||||
|
||||
#### Assignment
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
pass = 1
|
||||
```
|
||||
|
||||
#### Type alias
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
type pass = 1
|
||||
```
|
||||
|
||||
#### Function definition
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
def True(for):
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
pass
|
||||
```
|
||||
|
||||
# error: [invalid-syntax]
|
||||
#### For
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `pass` used when not defined"
|
||||
for while in pass:
|
||||
pass
|
||||
```
|
||||
|
||||
#### While
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `in` used when not defined"
|
||||
while in:
|
||||
pass
|
||||
```
|
||||
|
||||
#### Match
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `match` used when not defined"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Ellipsis literals
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
reveal_type(...) # revealed: EllipsisType | ellipsis
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
This test makes sure that `red_knot_test` correctly parses the TOML configuration blocks and applies
|
||||
the correct settings hierarchically.
|
||||
|
||||
The following configuration will be attached to the *root* section (without any heading):
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.10"
|
||||
```
|
||||
|
||||
# Basic
|
||||
|
||||
Here, we simply make sure that we pick up the global configuration from the root section:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
# Inheritance
|
||||
|
||||
## Child
|
||||
|
||||
### Grandchild
|
||||
|
||||
The same should work for arbitrarily nested sections:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
# Overwriting
|
||||
|
||||
Here, we make sure that we can overwrite the global configuration in a child section:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 11)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
# No global state
|
||||
|
||||
There is no global state. This section should again use the root configuration:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
# Overwriting affects children
|
||||
|
||||
Children in this section should all use the section configuration:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.12"
|
||||
```
|
||||
|
||||
## Child
|
||||
|
||||
### Grandchild
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 12)) # revealed: Literal[True]
|
||||
```
|
||||
@@ -179,9 +179,9 @@ reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
|
||||
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
|
||||
|
||||
```py path=a.pyi
|
||||
class A(B): ... # error: [cyclic-class-def]
|
||||
class B(C): ... # error: [cyclic-class-def]
|
||||
class C(A): ... # error: [cyclic-class-def]
|
||||
class A(B): ... # error: [cyclic-class-definition]
|
||||
class B(C): ... # error: [cyclic-class-definition]
|
||||
class C(A): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -348,14 +348,14 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
|
||||
These are invalid, but we need to be able to handle them gracefully without panicking.
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-def]
|
||||
class Foo(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar: ...
|
||||
class Baz: ...
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def]
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Boz) # revealed: Literal[Boz]
|
||||
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
|
||||
@@ -366,9 +366,9 @@ reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[objec
|
||||
These are similarly unlikely, but we still shouldn't crash:
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo): ... # error: [cyclic-class-def]
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
@@ -379,9 +379,9 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
|
||||
|
||||
```py path=a.pyi
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
@@ -391,16 +391,16 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
|
||||
## Classes with cycles in their MRO, and a sub-graph
|
||||
|
||||
```py path=a.pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||
class Bar(Foo): ...
|
||||
|
||||
# TODO: can we avoid emitting the errors for these?
|
||||
# The classes have cyclic superclasses,
|
||||
# but are not themselves cyclic...
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
|
||||
class Spam(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Spam(Baz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Nonlocal references
|
||||
|
||||
## One level up
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Two levels up
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
def h():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Skips class scope
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
x = 2
|
||||
def g():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Skips annotation-only assignment
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
# it's pretty weird to have an annotated assignment in a function where the
|
||||
# name is otherwise not defined; maybe should be an error?
|
||||
x: int
|
||||
def h():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
# Unbound
|
||||
|
||||
## Unbound class variable
|
||||
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
y = x
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Possibly unbound in class and global scope
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
x = "abc"
|
||||
|
||||
class C:
|
||||
if bool_instance():
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
|
||||
```
|
||||
|
||||
## Unbound function local
|
||||
|
||||
An unbound function local that has definitions in the scope does not fall back to globals.
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
reveal_type(x)
|
||||
x = 2
|
||||
# revealed: Literal[2]
|
||||
reveal_type(x)
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# Class defenitions in stubs
|
||||
# Class definitions in stubs
|
||||
|
||||
## Cyclical class definition
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# `sys.version_info`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.9"
|
||||
```
|
||||
|
||||
## The type of `sys.version_info`
|
||||
|
||||
The type of `sys.version_info` is `sys._version_info`, at least according to typeshed's stubs (which
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# type special form
|
||||
|
||||
## Class literal
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def f() -> type[A]:
|
||||
return A
|
||||
|
||||
reveal_type(f()) # revealed: type[A]
|
||||
```
|
||||
|
||||
## Nested class literal
|
||||
|
||||
```py
|
||||
class A:
|
||||
class B: ...
|
||||
|
||||
def f() -> type[A.B]:
|
||||
return A.B
|
||||
|
||||
reveal_type(f()) # revealed: type[B]
|
||||
```
|
||||
|
||||
## Deeply nested class literal
|
||||
|
||||
```py
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f() -> type[A.B.C]:
|
||||
return A.B.C
|
||||
|
||||
reveal_type(f()) # revealed: type[C]
|
||||
```
|
||||
|
||||
## Class literal from another module
|
||||
|
||||
```py
|
||||
from a import A
|
||||
|
||||
def f() -> type[A]:
|
||||
return A
|
||||
|
||||
reveal_type(f()) # revealed: type[A]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
class A: ...
|
||||
```
|
||||
|
||||
## Qualified class literal from another module
|
||||
|
||||
```py
|
||||
import a
|
||||
|
||||
def f() -> type[a.B]:
|
||||
return a.B
|
||||
|
||||
reveal_type(f()) # revealed: type[B]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
class B: ...
|
||||
```
|
||||
|
||||
## Deeply qualified class literal from another module
|
||||
|
||||
```py path=a/test.py
|
||||
import a.b
|
||||
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
def f() -> type[a.b.C]:
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
return a.b.C
|
||||
|
||||
reveal_type(f()) # revealed: @Todo(unsupported type[X] special form)
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Union of classes
|
||||
|
||||
```py
|
||||
class BasicUser: ...
|
||||
class ProUser: ...
|
||||
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def get_user() -> type[BasicUser | ProUser | A.B.C]:
|
||||
return BasicUser
|
||||
|
||||
# revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(get_user())
|
||||
```
|
||||
|
||||
## Illegal parameters
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def get_user() -> type[A, B]:
|
||||
return A
|
||||
```
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::lint::RuleSelection;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
@@ -5,6 +6,8 @@ use ruff_db::{Db as SourceDb, Upcast};
|
||||
#[salsa::db]
|
||||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
fn is_file_open(&self, file: File) -> bool;
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -13,23 +16,24 @@ pub(crate) mod tests {
|
||||
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::ProgramSettings;
|
||||
use crate::{default_lint_registry, ProgramSettings};
|
||||
|
||||
use super::Db;
|
||||
use crate::lint::RuleSelection;
|
||||
use anyhow::Context;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
use super::Db;
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
@@ -38,8 +42,9 @@ pub(crate) mod tests {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: std::sync::Arc::default(),
|
||||
events: Arc::default(),
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(&default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +107,10 @@ pub(crate) mod tests {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::hash::BuildHasherDefault;
|
||||
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
use crate::lint::{LintRegistry, LintRegistryBuilder};
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, Module};
|
||||
@@ -11,6 +12,7 @@ pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
||||
pub mod ast_node_ref;
|
||||
mod db;
|
||||
pub mod lint;
|
||||
mod module_name;
|
||||
mod module_resolver;
|
||||
mod node_key;
|
||||
@@ -26,3 +28,13 @@ mod unpack;
|
||||
mod util;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
pub fn default_lint_registry() -> LintRegistry {
|
||||
let mut registry = LintRegistryBuilder::default();
|
||||
register_semantic_lints(&mut registry);
|
||||
registry.build()
|
||||
}
|
||||
|
||||
pub fn register_semantic_lints(registry: &mut LintRegistryBuilder) {
|
||||
types::register_type_lints(registry);
|
||||
}
|
||||
|
||||
419
crates/red_knot_python_semantic/src/lint.rs
Normal file
419
crates/red_knot_python_semantic/src/lint.rs
Normal file
@@ -0,0 +1,419 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{LintName, Severity};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::hash::Hasher;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LintMetadata {
|
||||
/// The unique identifier for the lint.
|
||||
pub name: LintName,
|
||||
|
||||
/// A one-sentence summary of what the lint catches.
|
||||
pub summary: &'static str,
|
||||
|
||||
/// An in depth explanation of the lint in markdown. Covers what the lint does, why it's bad and possible fixes.
|
||||
///
|
||||
/// The documentation may require post-processing to be rendered correctly. For example, lines
|
||||
/// might have leading or trailing whitespace that should be removed.
|
||||
pub raw_documentation: &'static str,
|
||||
|
||||
/// The default level of the lint if the user doesn't specify one.
|
||||
pub default_level: Level,
|
||||
|
||||
pub status: LintStatus,
|
||||
|
||||
/// Location where this lint is declared: `file_name:line`
|
||||
pub source: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum Level {
|
||||
/// The lint is disabled and should not run.
|
||||
Ignore,
|
||||
|
||||
/// The lint is enabled and diagnostic should have a warning severity.
|
||||
Warn,
|
||||
|
||||
/// The lint is enabled and diagnostics have an error severity.
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Level {
|
||||
pub const fn is_error(self) -> bool {
|
||||
matches!(self, Level::Error)
|
||||
}
|
||||
|
||||
pub const fn is_warn(self) -> bool {
|
||||
matches!(self, Level::Warn)
|
||||
}
|
||||
|
||||
pub const fn is_ignore(self) -> bool {
|
||||
matches!(self, Level::Ignore)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Level> for Severity {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(level: Level) -> Result<Self, ()> {
|
||||
match level {
|
||||
Level::Ignore => Err(()),
|
||||
Level::Warn => Ok(Severity::Warning),
|
||||
Level::Error => Ok(Severity::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LintMetadata {
|
||||
pub fn name(&self) -> LintName {
|
||||
self.name
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> &str {
|
||||
self.summary
|
||||
}
|
||||
|
||||
/// Returns the documentation line by line with leading and trailing whitespace removed.
|
||||
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
|
||||
self.raw_documentation
|
||||
.lines()
|
||||
.map(|line| line.strip_prefix(' ').unwrap_or(line).trim_end())
|
||||
}
|
||||
|
||||
/// Returns the documentation as a single string.
|
||||
pub fn documentation(&self) -> String {
|
||||
self.documentation_lines().join("\n")
|
||||
}
|
||||
|
||||
pub fn default_level(&self) -> Level {
|
||||
self.default_level
|
||||
}
|
||||
|
||||
pub fn status(&self) -> &LintStatus {
|
||||
&self.status
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &str {
|
||||
self.source
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub const fn lint_metadata_defaults() -> LintMetadata {
|
||||
LintMetadata {
|
||||
name: LintName::of(""),
|
||||
summary: "",
|
||||
raw_documentation: "",
|
||||
default_level: Level::Error,
|
||||
status: LintStatus::preview("0.0.0"),
|
||||
source: "",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum LintStatus {
|
||||
/// The rule has been added to the linter, but is not yet stable.
|
||||
Preview {
|
||||
/// When the rule was added to preview
|
||||
since: &'static str,
|
||||
},
|
||||
|
||||
/// Stable rule that was added in the version defined by `since`.
|
||||
Stable { since: &'static str },
|
||||
|
||||
/// The rule has been deprecated [`since`] (version) and will be removed in the future.
|
||||
Deprecated {
|
||||
since: &'static str,
|
||||
reason: &'static str,
|
||||
},
|
||||
|
||||
/// The rule has been removed [`since`] (version) and using it will result in an error.
|
||||
Removed {
|
||||
since: &'static str,
|
||||
reason: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
impl LintStatus {
|
||||
pub const fn preview(since: &'static str) -> Self {
|
||||
LintStatus::Preview { since }
|
||||
}
|
||||
|
||||
pub const fn stable(since: &'static str) -> Self {
|
||||
LintStatus::Stable { since }
|
||||
}
|
||||
|
||||
pub const fn deprecated(since: &'static str, reason: &'static str) -> Self {
|
||||
LintStatus::Deprecated { since, reason }
|
||||
}
|
||||
|
||||
pub const fn removed(since: &'static str, reason: &'static str) -> Self {
|
||||
LintStatus::Removed { since, reason }
|
||||
}
|
||||
|
||||
pub const fn is_removed(&self) -> bool {
|
||||
matches!(self, LintStatus::Removed { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! declare_lint {
|
||||
(
|
||||
$(#[doc = $doc:literal])+
|
||||
$vis: vis static $name: ident = {
|
||||
summary: $summary: literal,
|
||||
status: $status: expr,
|
||||
// Optional properties
|
||||
$( $key:ident: $value:expr, )*
|
||||
}
|
||||
) => {
|
||||
$( #[doc = $doc] )+
|
||||
#[allow(clippy::needless_update)]
|
||||
$vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
|
||||
name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
|
||||
summary: $summary,
|
||||
raw_documentation: concat!($($doc,)+ "\n"),
|
||||
status: $status,
|
||||
source: concat!(file!(), ":", line!()),
|
||||
$( $key: $value, )*
|
||||
..$crate::lint::lint_metadata_defaults()
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/// A unique identifier for a lint rule.
|
||||
///
|
||||
/// Implements `PartialEq`, `Eq`, and `Hash` based on the `LintMetadata` pointer
|
||||
/// for fast comparison and lookup.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LintId {
|
||||
definition: &'static LintMetadata,
|
||||
}
|
||||
|
||||
impl LintId {
|
||||
pub const fn of(definition: &'static LintMetadata) -> Self {
|
||||
LintId { definition }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for LintId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::ptr::eq(self.definition, other.definition)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for LintId {}
|
||||
|
||||
impl std::hash::Hash for LintId {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
std::ptr::hash(self.definition, state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for LintId {
|
||||
type Target = LintMetadata;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.definition
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LintRegistryBuilder {
|
||||
/// Registered lints that haven't been removed.
|
||||
lints: Vec<LintId>,
|
||||
|
||||
/// Lints indexed by name, including aliases and removed rules.
|
||||
by_name: FxHashMap<&'static str, LintEntry>,
|
||||
}
|
||||
|
||||
impl LintRegistryBuilder {
|
||||
#[track_caller]
|
||||
pub fn register_lint(&mut self, lint: &'static LintMetadata) {
|
||||
assert_eq!(
|
||||
self.by_name.insert(&*lint.name, lint.into()),
|
||||
None,
|
||||
"duplicate lint registration for '{name}'",
|
||||
name = lint.name
|
||||
);
|
||||
|
||||
if !lint.status.is_removed() {
|
||||
self.lints.push(LintId::of(lint));
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn register_alias(&mut self, from: LintName, to: &'static LintMetadata) {
|
||||
let target = match self.by_name.get(to.name.as_str()) {
|
||||
Some(LintEntry::Lint(target) | LintEntry::Removed(target)) => target,
|
||||
Some(LintEntry::Alias(target)) => {
|
||||
panic!(
|
||||
"lint alias {from} -> {to:?} points to another alias {target:?}",
|
||||
target = target.name()
|
||||
)
|
||||
}
|
||||
None => panic!(
|
||||
"lint alias {from} -> {to} points to non-registered lint",
|
||||
to = to.name
|
||||
),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
self.by_name
|
||||
.insert(from.as_str(), LintEntry::Alias(*target)),
|
||||
None,
|
||||
"duplicate lint registration for '{from}'",
|
||||
);
|
||||
}
|
||||
|
||||
pub fn build(self) -> LintRegistry {
|
||||
LintRegistry {
|
||||
lints: self.lints,
|
||||
by_name: self.by_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LintRegistry {
|
||||
lints: Vec<LintId>,
|
||||
by_name: FxHashMap<&'static str, LintEntry>,
|
||||
}
|
||||
|
||||
impl LintRegistry {
|
||||
/// Looks up a lint by its name.
|
||||
pub fn get(&self, code: &str) -> Result<LintId, GetLintError> {
|
||||
match self.by_name.get(code) {
|
||||
Some(LintEntry::Lint(metadata)) => Ok(*metadata),
|
||||
Some(LintEntry::Alias(lint)) => {
|
||||
if lint.status.is_removed() {
|
||||
Err(GetLintError::Removed(lint.name()))
|
||||
} else {
|
||||
Ok(*lint)
|
||||
}
|
||||
}
|
||||
Some(LintEntry::Removed(lint)) => Err(GetLintError::Removed(lint.name())),
|
||||
None => Err(GetLintError::Unknown(code.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all registered, non-removed lints.
|
||||
pub fn lints(&self) -> &[LintId] {
|
||||
&self.lints
|
||||
}
|
||||
|
||||
/// Returns an iterator over all known aliases and to their target lints.
|
||||
///
|
||||
/// This iterator includes aliases that point to removed lints.
|
||||
pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + use<'_> {
|
||||
self.by_name.iter().filter_map(|(key, value)| {
|
||||
if let LintEntry::Alias(alias) = value {
|
||||
Some((LintName::of(key), *alias))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterates over all removed lints.
|
||||
pub fn removed(&self) -> impl Iterator<Item = LintId> + use<'_> {
|
||||
self.by_name.iter().filter_map(|(_, value)| {
|
||||
if let LintEntry::Removed(metadata) = value {
|
||||
Some(*metadata)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum GetLintError {
|
||||
/// The name maps to this removed lint.
|
||||
#[error("lint {0} has been removed")]
|
||||
Removed(LintName),
|
||||
|
||||
/// No lint with the given name is known.
|
||||
#[error("unknown lint {0}")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum LintEntry {
|
||||
/// An existing lint rule. Can be in preview, stable or deprecated.
|
||||
Lint(LintId),
|
||||
/// A lint rule that has been removed.
|
||||
Removed(LintId),
|
||||
Alias(LintId),
|
||||
}
|
||||
|
||||
impl From<&'static LintMetadata> for LintEntry {
|
||||
fn from(metadata: &'static LintMetadata) -> Self {
|
||||
if metadata.status.is_removed() {
|
||||
LintEntry::Removed(LintId::of(metadata))
|
||||
} else {
|
||||
LintEntry::Lint(LintId::of(metadata))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RuleSelection {
|
||||
/// Map with the severity for each enabled lint rule.
|
||||
///
|
||||
/// If a rule isn't present in this map, then it should be considered disabled.
|
||||
lints: FxHashMap<LintId, Severity>,
|
||||
}
|
||||
|
||||
impl RuleSelection {
|
||||
/// Creates a new rule selection from all known lints in the registry that are enabled
|
||||
/// according to their default severity.
|
||||
pub fn from_registry(registry: &LintRegistry) -> Self {
|
||||
let lints = registry
|
||||
.lints()
|
||||
.iter()
|
||||
.filter_map(|lint| {
|
||||
Severity::try_from(lint.default_level())
|
||||
.ok()
|
||||
.map(|severity| (*lint, severity))
|
||||
})
|
||||
.collect();
|
||||
|
||||
RuleSelection { lints }
|
||||
}
|
||||
|
||||
/// Returns an iterator over all enabled lints.
|
||||
pub fn enabled(&self) -> impl Iterator<Item = LintId> + use<'_> {
|
||||
self.lints.keys().copied()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all enabled lints and their severity.
|
||||
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + use<'_> {
|
||||
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
|
||||
}
|
||||
|
||||
/// Returns the configured severity for the lint with the given id or `None` if the lint is disabled.
|
||||
pub fn severity(&self, lint: LintId) -> Option<Severity> {
|
||||
self.lints.get(&lint).copied()
|
||||
}
|
||||
|
||||
/// Enables `lint` and configures with the given `severity`.
|
||||
///
|
||||
/// Overrides any previous configuration for the lint.
|
||||
pub fn enable(&mut self, lint: LintId, severity: Severity) {
|
||||
self.lints.insert(lint, severity);
|
||||
}
|
||||
|
||||
/// Disables `lint` if it was previously enabled.
|
||||
pub fn disable(&mut self, lint: LintId) {
|
||||
self.lints.remove(&lint);
|
||||
}
|
||||
|
||||
/// Merges the enabled lints from `other` into this selection.
|
||||
///
|
||||
/// Lints from `other` will override any existing configuration.
|
||||
pub fn merge(&mut self, other: &RuleSelection) {
|
||||
self.lints.extend(other.iter());
|
||||
}
|
||||
}
|
||||
@@ -606,24 +606,11 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
let function_table = index.symbol_table(function_scope_id);
|
||||
assert_eq!(
|
||||
names(&function_table),
|
||||
vec!["a", "b", "c", "args", "d", "kwargs"],
|
||||
vec!["a", "b", "c", "d", "args", "kwargs"],
|
||||
);
|
||||
|
||||
let use_def = index.use_def_map(function_scope_id);
|
||||
for name in ["a", "b", "c", "d"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(
|
||||
function_table
|
||||
.symbol_id_by_name(name)
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
binding.kind(&db),
|
||||
DefinitionKind::ParameterWithDefault(_)
|
||||
));
|
||||
}
|
||||
for name in ["args", "kwargs"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(
|
||||
function_table
|
||||
@@ -633,6 +620,28 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
|
||||
}
|
||||
let args_binding = use_def
|
||||
.first_public_binding(
|
||||
function_table
|
||||
.symbol_id_by_name("args")
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
args_binding.kind(&db),
|
||||
DefinitionKind::VariadicPositionalParameter(_)
|
||||
));
|
||||
let kwargs_binding = use_def
|
||||
.first_public_binding(
|
||||
function_table
|
||||
.symbol_id_by_name("kwargs")
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
kwargs_binding.kind(&db),
|
||||
DefinitionKind::VariadicKeywordParameter(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -654,25 +663,38 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
let lambda_table = index.symbol_table(lambda_scope_id);
|
||||
assert_eq!(
|
||||
names(&lambda_table),
|
||||
vec!["a", "b", "c", "args", "d", "kwargs"],
|
||||
vec!["a", "b", "c", "d", "args", "kwargs"],
|
||||
);
|
||||
|
||||
let use_def = index.use_def_map(lambda_scope_id);
|
||||
for name in ["a", "b", "c", "d"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
binding.kind(&db),
|
||||
DefinitionKind::ParameterWithDefault(_)
|
||||
));
|
||||
}
|
||||
for name in ["args", "kwargs"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
|
||||
}
|
||||
let args_binding = use_def
|
||||
.first_public_binding(
|
||||
lambda_table
|
||||
.symbol_id_by_name("args")
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
args_binding.kind(&db),
|
||||
DefinitionKind::VariadicPositionalParameter(_)
|
||||
));
|
||||
let kwargs_binding = use_def
|
||||
.first_public_binding(
|
||||
lambda_table
|
||||
.symbol_id_by_name("kwargs")
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
kwargs_binding.kind(&db),
|
||||
DefinitionKind::VariadicKeywordParameter(_)
|
||||
));
|
||||
}
|
||||
|
||||
/// Test case to validate that the comprehension scope is correctly identified and that the target
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_index::IndexVec;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
|
||||
use ruff_python_ast::{AnyParameterRef, BoolOp, Expr};
|
||||
use ruff_python_ast::{BoolOp, Expr};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
@@ -479,21 +479,35 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.pop_scope();
|
||||
}
|
||||
|
||||
fn declare_parameter(&mut self, parameter: AnyParameterRef<'db>) {
|
||||
let symbol = self.add_symbol(parameter.name().id().clone());
|
||||
fn declare_parameters(&mut self, parameters: &'db ast::Parameters) {
|
||||
for parameter in parameters.iter_non_variadic_params() {
|
||||
self.declare_parameter(parameter);
|
||||
}
|
||||
if let Some(vararg) = parameters.vararg.as_ref() {
|
||||
let symbol = self.add_symbol(vararg.name.id().clone());
|
||||
self.add_definition(
|
||||
symbol,
|
||||
DefinitionNodeRef::VariadicPositionalParameter(vararg),
|
||||
);
|
||||
}
|
||||
if let Some(kwarg) = parameters.kwarg.as_ref() {
|
||||
let symbol = self.add_symbol(kwarg.name.id().clone());
|
||||
self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg));
|
||||
}
|
||||
}
|
||||
|
||||
fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) {
|
||||
let symbol = self.add_symbol(parameter.parameter.name.id().clone());
|
||||
|
||||
let definition = self.add_definition(symbol, parameter);
|
||||
|
||||
if let AnyParameterRef::NonVariadic(with_default) = parameter {
|
||||
// Insert a mapping from the parameter to the same definition.
|
||||
// This ensures that calling `HasTy::ty` on the inner parameter returns
|
||||
// a valid type (and doesn't panic)
|
||||
let existing_definition = self.definitions_by_node.insert(
|
||||
DefinitionNodeRef::from(AnyParameterRef::Variadic(&with_default.parameter)).key(),
|
||||
definition,
|
||||
);
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
}
|
||||
// Insert a mapping from the inner Parameter node to the same definition.
|
||||
// This ensures that calling `HasTy::ty` on the inner parameter returns
|
||||
// a valid type (and doesn't panic)
|
||||
let existing_definition = self
|
||||
.definitions_by_node
|
||||
.insert((¶meter.parameter).into(), definition);
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
}
|
||||
|
||||
pub(super) fn build(mut self) -> SemanticIndex<'db> {
|
||||
@@ -556,34 +570,40 @@ where
|
||||
fn visit_stmt(&mut self, stmt: &'ast ast::Stmt) {
|
||||
match stmt {
|
||||
ast::Stmt::FunctionDef(function_def) => {
|
||||
for decorator in &function_def.decorator_list {
|
||||
let ast::StmtFunctionDef {
|
||||
decorator_list,
|
||||
parameters,
|
||||
type_params,
|
||||
name,
|
||||
returns,
|
||||
body,
|
||||
is_async: _,
|
||||
range: _,
|
||||
} = function_def;
|
||||
for decorator in decorator_list {
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
||||
function_def.type_params.as_deref(),
|
||||
type_params.as_deref(),
|
||||
|builder| {
|
||||
builder.visit_parameters(&function_def.parameters);
|
||||
if let Some(expr) = &function_def.returns {
|
||||
builder.visit_annotation(expr);
|
||||
builder.visit_parameters(parameters);
|
||||
if let Some(returns) = returns {
|
||||
builder.visit_annotation(returns);
|
||||
}
|
||||
|
||||
builder.push_scope(NodeWithScopeRef::Function(function_def));
|
||||
|
||||
// Add symbols and definitions for the parameters to the function scope.
|
||||
for parameter in &*function_def.parameters {
|
||||
builder.declare_parameter(parameter);
|
||||
}
|
||||
builder.declare_parameters(parameters);
|
||||
|
||||
builder.visit_body(&function_def.body);
|
||||
builder.visit_body(body);
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
// The default value of the parameters needs to be evaluated in the
|
||||
// enclosing scope.
|
||||
for default in function_def
|
||||
.parameters
|
||||
for default in parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
@@ -592,7 +612,7 @@ where
|
||||
// The symbol for the function name itself has to be evaluated
|
||||
// at the end to match the runtime evaluation of parameter defaults
|
||||
// and return-type annotations.
|
||||
let symbol = self.add_symbol(function_def.name.id.clone());
|
||||
let symbol = self.add_symbol(name.id.clone());
|
||||
self.add_definition(symbol, function_def);
|
||||
}
|
||||
ast::Stmt::ClassDef(class) => {
|
||||
@@ -1179,10 +1199,8 @@ where
|
||||
self.push_scope(NodeWithScopeRef::Lambda(lambda));
|
||||
|
||||
// Add symbols and definitions for the parameters to the lambda scope.
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
for parameter in parameters {
|
||||
self.declare_parameter(parameter);
|
||||
}
|
||||
if let Some(parameters) = lambda.parameters.as_ref() {
|
||||
self.declare_parameters(parameters);
|
||||
}
|
||||
|
||||
self.visit_expr(lambda.body.as_ref());
|
||||
|
||||
@@ -89,7 +89,9 @@ pub(crate) enum DefinitionNodeRef<'a> {
|
||||
AnnotatedAssignment(&'a ast::StmtAnnAssign),
|
||||
AugmentedAssignment(&'a ast::StmtAugAssign),
|
||||
Comprehension(ComprehensionDefinitionNodeRef<'a>),
|
||||
Parameter(ast::AnyParameterRef<'a>),
|
||||
VariadicPositionalParameter(&'a ast::Parameter),
|
||||
VariadicKeywordParameter(&'a ast::Parameter),
|
||||
Parameter(&'a ast::ParameterWithDefault),
|
||||
WithItem(WithItemDefinitionNodeRef<'a>),
|
||||
MatchPattern(MatchPatternDefinitionNodeRef<'a>),
|
||||
ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>),
|
||||
@@ -188,8 +190,8 @@ impl<'a> From<ComprehensionDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ast::AnyParameterRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node: ast::AnyParameterRef<'a>) -> Self {
|
||||
impl<'a> From<&'a ast::ParameterWithDefault> for DefinitionNodeRef<'a> {
|
||||
fn from(node: &'a ast::ParameterWithDefault) -> Self {
|
||||
Self::Parameter(node)
|
||||
}
|
||||
}
|
||||
@@ -315,14 +317,15 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::Parameter(parameter) => match parameter {
|
||||
ast::AnyParameterRef::Variadic(parameter) => {
|
||||
DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
ast::AnyParameterRef::NonVariadic(parameter) => {
|
||||
DefinitionKind::ParameterWithDefault(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
},
|
||||
DefinitionNodeRef::VariadicPositionalParameter(parameter) => {
|
||||
DefinitionKind::VariadicPositionalParameter(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
DefinitionNodeRef::VariadicKeywordParameter(parameter) => {
|
||||
DefinitionKind::VariadicKeywordParameter(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
DefinitionNodeRef::Parameter(parameter) => {
|
||||
DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef {
|
||||
node,
|
||||
target,
|
||||
@@ -384,10 +387,9 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
is_async: _,
|
||||
}) => target.into(),
|
||||
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
|
||||
Self::Parameter(node) => match node {
|
||||
ast::AnyParameterRef::Variadic(parameter) => parameter.into(),
|
||||
ast::AnyParameterRef::NonVariadic(parameter) => parameter.into(),
|
||||
},
|
||||
Self::VariadicPositionalParameter(node) => node.into(),
|
||||
Self::VariadicKeywordParameter(node) => node.into(),
|
||||
Self::Parameter(node) => node.into(),
|
||||
Self::WithItem(WithItemDefinitionNodeRef {
|
||||
node: _,
|
||||
target,
|
||||
@@ -452,8 +454,9 @@ pub enum DefinitionKind<'db> {
|
||||
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
||||
For(ForStmtDefinitionKind),
|
||||
Comprehension(ComprehensionDefinitionKind),
|
||||
Parameter(AstNodeRef<ast::Parameter>),
|
||||
ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>),
|
||||
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
|
||||
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
|
||||
Parameter(AstNodeRef<ast::ParameterWithDefault>),
|
||||
WithItem(WithItemDefinitionKind),
|
||||
MatchPattern(MatchPatternDefinitionKind),
|
||||
ExceptHandler(ExceptHandlerDefinitionKind),
|
||||
@@ -475,7 +478,8 @@ impl DefinitionKind<'_> {
|
||||
| DefinitionKind::ParamSpec(_)
|
||||
| DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding,
|
||||
// a parameter always binds a value, but is only a declaration if annotated
|
||||
DefinitionKind::Parameter(parameter) => {
|
||||
DefinitionKind::VariadicPositionalParameter(parameter)
|
||||
| DefinitionKind::VariadicKeywordParameter(parameter) => {
|
||||
if parameter.annotation.is_some() {
|
||||
DefinitionCategory::DeclarationAndBinding
|
||||
} else {
|
||||
@@ -483,7 +487,7 @@ impl DefinitionKind<'_> {
|
||||
}
|
||||
}
|
||||
// presence of a default is irrelevant, same logic as for a no-default parameter
|
||||
DefinitionKind::ParameterWithDefault(parameter_with_default) => {
|
||||
DefinitionKind::Parameter(parameter_with_default) => {
|
||||
if parameter_with_default.parameter.annotation.is_some() {
|
||||
DefinitionCategory::DeclarationAndBinding
|
||||
} else {
|
||||
@@ -743,6 +747,15 @@ impl From<&ast::ParameterWithDefault> for DefinitionNodeKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ast::AnyParameterRef<'_>> for DefinitionNodeKey {
|
||||
fn from(value: ast::AnyParameterRef) -> Self {
|
||||
Self(match value {
|
||||
ast::AnyParameterRef::Variadic(node) => NodeKey::from_node(node),
|
||||
ast::AnyParameterRef::NonVariadic(node) => NodeKey::from_node(node),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::Identifier> for DefinitionNodeKey {
|
||||
fn from(identifier: &ast::Identifier) -> Self {
|
||||
Self(NodeKey::from_node(identifier))
|
||||
|
||||
@@ -2,11 +2,12 @@ use std::hash::Hash;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_db::diagnostic::{DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||
pub(crate) use self::diagnostic::register_type_lints;
|
||||
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
pub(crate) use self::display::TypeArrayDisplay;
|
||||
pub(crate) use self::infer::{
|
||||
@@ -25,7 +26,7 @@ use crate::stdlib::{
|
||||
builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule,
|
||||
};
|
||||
use crate::symbol::{Boundness, Symbol};
|
||||
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder;
|
||||
use crate::types::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
|
||||
use crate::types::mro::{ClassBase, Mro, MroError, MroIterator};
|
||||
use crate::types::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
|
||||
@@ -225,6 +226,18 @@ fn definition_expression_ty<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the type of an expression from an arbitrary scope.
|
||||
///
|
||||
/// Can cause query cycles if used carelessly; caller must be sure that type inference isn't
|
||||
/// currently in progress for the expression's scope.
|
||||
fn expression_ty<'db>(db: &'db dyn Db, file: File, expression: &ast::Expr) -> Type<'db> {
|
||||
let index = semantic_index(db, file);
|
||||
let file_scope = index.expression_scope_id(expression);
|
||||
let scope = file_scope.to_scope_id(db, file);
|
||||
let expr_id = expression.scoped_expression_id(db, scope);
|
||||
infer_scope_types(db, scope).expression_ty(expr_id)
|
||||
}
|
||||
|
||||
/// Infer the combined type of an iterator of bindings.
|
||||
///
|
||||
/// Will return a union if there is more than one binding.
|
||||
@@ -2288,9 +2301,9 @@ impl<'db> CallOutcome<'db> {
|
||||
not_callable_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
not_callable_ty.display(db)
|
||||
@@ -2303,9 +2316,9 @@ impl<'db> CallOutcome<'db> {
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union element `{}`)",
|
||||
called_ty.display(db),
|
||||
@@ -2319,9 +2332,9 @@ impl<'db> CallOutcome<'db> {
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union elements {})",
|
||||
called_ty.display(db),
|
||||
@@ -2334,9 +2347,9 @@ impl<'db> CallOutcome<'db> {
|
||||
callable_ty: called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
|
||||
called_ty.display(db)
|
||||
@@ -2362,7 +2375,8 @@ impl<'db> CallOutcome<'db> {
|
||||
} => {
|
||||
diagnostics.add(
|
||||
node,
|
||||
"revealed-type",
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
|
||||
);
|
||||
Ok(*return_ty)
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use crate::lint::{Level, LintId, LintMetadata, LintRegistryBuilder, LintStatus};
|
||||
use crate::types::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
RAW_STRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use crate::types::{ClassLiteralType, Type};
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use crate::{declare_lint, Db};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -9,18 +15,411 @@ use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Registers all known type check lints.
|
||||
pub(crate) fn register_type_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&UNRESOLVED_REFERENCE);
|
||||
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
|
||||
registry.register_lint(&NOT_ITERABLE);
|
||||
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
|
||||
registry.register_lint(&NON_SUBSCRIPTABLE);
|
||||
registry.register_lint(&UNRESOLVED_IMPORT);
|
||||
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
|
||||
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
|
||||
registry.register_lint(&INVALID_ASSIGNMENT);
|
||||
registry.register_lint(&INVALID_DECLARATION);
|
||||
registry.register_lint(&CONFLICTING_DECLARATIONS);
|
||||
registry.register_lint(&DIVISION_BY_ZERO);
|
||||
registry.register_lint(&CALL_NON_CALLABLE);
|
||||
registry.register_lint(&INVALID_TYPE_PARAMETER);
|
||||
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
|
||||
registry.register_lint(&CYCLIC_CLASS_DEFINITION);
|
||||
registry.register_lint(&DUPLICATE_BASE);
|
||||
registry.register_lint(&INVALID_BASE);
|
||||
registry.register_lint(&INCONSISTENT_MRO);
|
||||
registry.register_lint(&INVALID_LITERAL_PARAMETER);
|
||||
registry.register_lint(&CALL_POSSIBLY_UNBOUND_METHOD);
|
||||
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
|
||||
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
|
||||
registry.register_lint(&CONFLICTING_METACLASS);
|
||||
registry.register_lint(&UNSUPPORTED_OPERATOR);
|
||||
registry.register_lint(&INVALID_CONTEXT_MANAGER);
|
||||
registry.register_lint(&UNDEFINED_REVEAL);
|
||||
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
|
||||
registry.register_lint(&INVALID_TYPE_FORM);
|
||||
|
||||
// String annotations
|
||||
registry.register_lint(&FSTRING_TYPE_ANNOTATION);
|
||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||
registry.register_lint(&RAW_STRING_TYPE_ANNOTATION);
|
||||
registry.register_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION);
|
||||
registry.register_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION);
|
||||
registry.register_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION);
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for references to names that are not defined.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using an undefined variable will raise a `NameError` at runtime.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// print(x) # NameError: name 'x' is not defined
|
||||
/// ```
|
||||
pub(crate) static UNRESOLVED_REFERENCE = {
|
||||
summary: "detects references to names that are not defined",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Warn,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for references to names that are possibly not defined..
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using an undefined variable will raise a `NameError` at runtime.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// for i in range(0):
|
||||
/// x = i
|
||||
///
|
||||
/// print(x) # NameError: name 'x' is not defined
|
||||
/// ```
|
||||
pub(crate) static POSSIBLY_UNRESOLVED_REFERENCE = {
|
||||
summary: "detects references to possibly unresolved references",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Warn,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for objects that are not iterable but are used in a context that requires them to be.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Iterating over an object that is not iterable will raise a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// for i in 34: # TypeError: 'int' object is not iterable
|
||||
/// pass
|
||||
/// ```
|
||||
pub(crate) static NOT_ITERABLE = {
|
||||
summary: "detects objects that are not iterable",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// TODO
|
||||
pub(crate) static INDEX_OUT_OF_BOUNDS = {
|
||||
summary: "detects index out of bounds errors",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for objects that do not support subscripting but are used in a context that requires them to be.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Subscripting an object that does not support it will raise a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// 4[1] # TypeError: 'int' object is not subscriptable
|
||||
/// ```
|
||||
pub(crate) static NON_SUBSCRIPTABLE = {
|
||||
summary: "detects objects that do not support subscripting",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for import statements for which the module cannot be resolved.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Importing a module that cannot be resolved will raise an `ImportError` at runtime.
|
||||
pub(crate) static UNRESOLVED_IMPORT = {
|
||||
summary: "detects unresolved imports",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static POSSIBLY_UNBOUND_IMPORT = {
|
||||
summary: "detects possibly unbound imports",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Warn,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for step size 0 in slices.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// A slice with a step size of zero will raise a `ValueError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// l = list(range(10))
|
||||
/// l[1:10:0] # ValueError: slice step cannot be zero
|
||||
pub(crate) static ZERO_STEPSIZE_IN_SLICE = {
|
||||
summary: "detects a slice step size of zero",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static INVALID_ASSIGNMENT = {
|
||||
summary: "detects invalid assignments",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static INVALID_DECLARATION = {
|
||||
summary: "detects invalid declarations",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static CONFLICTING_DECLARATIONS = {
|
||||
summary: "detects conflicting declarations",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// It detects division by zero.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// 5 / 0
|
||||
/// ```
|
||||
pub(crate) static DIVISION_BY_ZERO = {
|
||||
summary: "detects division by zero",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls to non-callable objects.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// 4() # TypeError: 'int' object is not callable
|
||||
/// ```
|
||||
pub(crate) static CALL_NON_CALLABLE = {
|
||||
summary: "detects calls to non-callable objects",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// TODO
|
||||
pub(crate) static INVALID_TYPE_PARAMETER = {
|
||||
summary: "detects invalid type parameters",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static INVALID_TYPE_VARIABLE_CONSTRAINTS = {
|
||||
summary: "detects invalid type variable constraints",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for class definitions with a cyclic inheritance chain.
|
||||
///
|
||||
/// ## Why is it bad?
|
||||
/// TODO
|
||||
pub(crate) static CYCLIC_CLASS_DEFINITION = {
|
||||
summary: "detects cyclic class definitions",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static DUPLICATE_BASE = {
|
||||
summary: "detects class definitions with duplicate bases",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static INVALID_BASE = {
|
||||
summary: "detects class definitions with an invalid base",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static INCONSISTENT_MRO = {
|
||||
summary: "detects class definitions with an inconsistent MRO",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for invalid parameters to `typing.Literal`.
|
||||
pub(crate) static INVALID_LITERAL_PARAMETER = {
|
||||
summary: "detects invalid literal parameters",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls to possibly unbound methods.
|
||||
pub(crate) static CALL_POSSIBLY_UNBOUND_METHOD = {
|
||||
summary: "detects calls to possibly unbound methods",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Warn,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for possibly unbound attributes.
|
||||
pub(crate) static POSSIBLY_UNBOUND_ATTRIBUTE = {
|
||||
summary: "detects references to possibly unbound attributes",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Warn,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for unresolved attributes.
|
||||
pub(crate) static UNRESOLVED_ATTRIBUTE = {
|
||||
summary: "detects references to unresolved attributes",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static CONFLICTING_METACLASS = {
|
||||
summary: "detects conflicting metaclasses",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for binary expressions, comparisons, and unary expressions where the operands don't support the operator.
|
||||
pub(crate) static UNSUPPORTED_OPERATOR = {
|
||||
summary: "detects binary expressions where the operands don't support the operator",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static INVALID_CONTEXT_MANAGER = {
|
||||
summary: "detects expressions used in with statements that don't implement the context manager protocol",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls to `reveal_type` without importing it.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using `reveal_type` without importing it will raise a `NameError` at runtime.
|
||||
pub(crate) static UNDEFINED_REVEAL = {
|
||||
summary: "detects usages of `reveal_type` without importing it",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Warn,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for default values that can't be assigned to the parameter's annotated type.
|
||||
pub(crate) static INVALID_PARAMETER_DEFAULT = {
|
||||
summary: "detects default values that can't be assigned to the parameter's annotated type",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for `type[]` usages that have too many or too few type arguments.
|
||||
pub(crate) static INVALID_TYPE_FORM = {
|
||||
summary: "detects invalid type forms",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
// TODO: Don't use string keys for rules
|
||||
pub(super) rule: String,
|
||||
pub(super) id: DiagnosticId,
|
||||
pub(super) message: String,
|
||||
pub(super) range: TextRange,
|
||||
pub(super) severity: Severity,
|
||||
pub(super) file: File,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostic {
|
||||
pub fn rule(&self) -> &str {
|
||||
&self.rule
|
||||
pub fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &str {
|
||||
@@ -33,8 +432,8 @@ impl TypeCheckDiagnostic {
|
||||
}
|
||||
|
||||
impl Diagnostic for TypeCheckDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
TypeCheckDiagnostic::rule(self)
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
@@ -50,7 +449,7 @@ impl Diagnostic for TypeCheckDiagnostic {
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
self.severity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,9 +549,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
|
||||
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
|
||||
pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&NOT_ITERABLE,
|
||||
node,
|
||||
"not-iterable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable",
|
||||
not_iterable_ty.display(self.db)
|
||||
@@ -167,9 +566,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
node: AnyNodeRef,
|
||||
element_ty: Type<'db>,
|
||||
) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&NOT_ITERABLE,
|
||||
node,
|
||||
"not-iterable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
|
||||
element_ty.display(self.db)
|
||||
@@ -186,9 +585,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
length: usize,
|
||||
index: i64,
|
||||
) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&INDEX_OUT_OF_BOUNDS,
|
||||
node,
|
||||
"index-out-of-bounds",
|
||||
format_args!(
|
||||
"Index {index} is out of bounds for {kind} `{}` with length {length}",
|
||||
tuple_ty.display(self.db)
|
||||
@@ -203,9 +602,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
non_subscriptable_ty: Type<'db>,
|
||||
method: &str,
|
||||
) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&NON_SUBSCRIPTABLE,
|
||||
node,
|
||||
"non-subscriptable",
|
||||
format_args!(
|
||||
"Cannot subscript object of type `{}` with no `{method}` method",
|
||||
non_subscriptable_ty.display(self.db)
|
||||
@@ -219,9 +618,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
level: u32,
|
||||
module: Option<&str>,
|
||||
) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&UNRESOLVED_IMPORT,
|
||||
import_node.into(),
|
||||
"unresolved-import",
|
||||
format_args!(
|
||||
"Cannot resolve import `{}{}`",
|
||||
".".repeat(level as usize),
|
||||
@@ -231,9 +630,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
}
|
||||
|
||||
pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&ZERO_STEPSIZE_IN_SLICE,
|
||||
node,
|
||||
"zero-stepsize-in-slice",
|
||||
format_args!("Slice step size can not be zero"),
|
||||
);
|
||||
}
|
||||
@@ -246,19 +645,19 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
) {
|
||||
match declared_ty {
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
self.add(node, "invalid-assignment", format_args!(
|
||||
self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
|
||||
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
|
||||
class.name(self.db)));
|
||||
}
|
||||
Type::FunctionLiteral(function) => {
|
||||
self.add(node, "invalid-assignment", format_args!(
|
||||
self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
|
||||
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
|
||||
function.name(self.db)));
|
||||
}
|
||||
_ => {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
node,
|
||||
"invalid-assignment",
|
||||
format_args!(
|
||||
"Object of type `{}` is not assignable to `{}`",
|
||||
assigned_ty.display(self.db),
|
||||
@@ -272,9 +671,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&POSSIBLY_UNRESOLVED_REFERENCE,
|
||||
expr_name_node.into(),
|
||||
"possibly-unresolved-reference",
|
||||
format_args!("Name `{id}` used when possibly not defined"),
|
||||
);
|
||||
}
|
||||
@@ -282,17 +681,37 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&UNRESOLVED_REFERENCE,
|
||||
expr_name_node.into(),
|
||||
"unresolved-reference",
|
||||
format_args!("Name `{id}` used when not defined"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_lint(
|
||||
&mut self,
|
||||
lint: &'static LintMetadata,
|
||||
node: AnyNodeRef,
|
||||
message: std::fmt::Arguments,
|
||||
) {
|
||||
// Skip over diagnostics if the rule is disabled.
|
||||
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.add(node, DiagnosticId::Lint(lint.name()), severity, message);
|
||||
}
|
||||
|
||||
/// Adds a new diagnostic.
|
||||
///
|
||||
/// The diagnostic does not get added if the rule isn't enabled for this file.
|
||||
pub(super) fn add(&mut self, node: AnyNodeRef, rule: &str, message: std::fmt::Arguments) {
|
||||
pub(super) fn add(
|
||||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: std::fmt::Arguments,
|
||||
) {
|
||||
if !self.db.is_file_open(self.file) {
|
||||
return;
|
||||
}
|
||||
@@ -305,9 +724,10 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
|
||||
self.diagnostics.push(TypeCheckDiagnostic {
|
||||
file: self.file,
|
||||
rule: rule.to_string(),
|
||||
id,
|
||||
message: message.to_string(),
|
||||
range: node.range(),
|
||||
severity,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ use std::num::NonZeroU32;
|
||||
use itertools::Itertools;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, UnaryOp};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use salsa;
|
||||
use salsa::plumbing::AsId;
|
||||
@@ -48,7 +48,15 @@ use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::stdlib::builtins_module_scope;
|
||||
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::types::diagnostic::{
|
||||
TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE,
|
||||
CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
|
||||
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_BASE,
|
||||
INVALID_CONTEXT_MANAGER, INVALID_DECLARATION, INVALID_LITERAL_PARAMETER,
|
||||
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_PARAMETER,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
|
||||
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
|
||||
};
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
@@ -63,7 +71,10 @@ use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
|
||||
use super::string_annotation::parse_string_annotation;
|
||||
use super::expression_ty;
|
||||
use super::string_annotation::{
|
||||
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
};
|
||||
|
||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
||||
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||
@@ -501,9 +512,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
for (class, class_node) in class_definitions {
|
||||
// (1) Check that the class does not have a cyclic definition
|
||||
if class.is_cyclically_defined(self.db) {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CYCLIC_CLASS_DEFINITION,
|
||||
class_node.into(),
|
||||
"cyclic-class-def",
|
||||
format_args!(
|
||||
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
|
||||
class.name(self.db),
|
||||
@@ -521,9 +532,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
let base_nodes = class_node.bases();
|
||||
for (index, duplicate) in duplicates {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&DUPLICATE_BASE,
|
||||
(&base_nodes[*index]).into(),
|
||||
"duplicate-base",
|
||||
format_args!("Duplicate base class `{}`", duplicate.name(self.db)),
|
||||
);
|
||||
}
|
||||
@@ -531,9 +542,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
MroErrorKind::InvalidBases(bases) => {
|
||||
let base_nodes = class_node.bases();
|
||||
for (index, base_ty) in bases {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_BASE,
|
||||
(&base_nodes[*index]).into(),
|
||||
"invalid-base",
|
||||
format_args!(
|
||||
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
|
||||
base_ty.display(self.db)
|
||||
@@ -541,9 +552,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
}
|
||||
}
|
||||
MroErrorKind::UnresolvableMro { bases_list } => self.diagnostics.add(
|
||||
MroErrorKind::UnresolvableMro { bases_list } => self.diagnostics.add_lint(
|
||||
&INCONSISTENT_MRO,
|
||||
class_node.into(),
|
||||
"inconsistent-mro",
|
||||
format_args!(
|
||||
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
|
||||
class.name(self.db),
|
||||
@@ -571,9 +582,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} => {
|
||||
let node = class_node.into();
|
||||
if *candidate1_is_base_class {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CONFLICTING_METACLASS,
|
||||
node,
|
||||
"conflicting-metaclass",
|
||||
format_args!(
|
||||
"The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \
|
||||
but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \
|
||||
@@ -583,12 +594,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
base1 = class1.name(self.db),
|
||||
metaclass2 = metaclass2.name(self.db),
|
||||
base2 = class2.name(self.db),
|
||||
)
|
||||
),
|
||||
);
|
||||
} else {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CONFLICTING_METACLASS,
|
||||
node,
|
||||
"conflicting-metaclass",
|
||||
format_args!(
|
||||
"The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \
|
||||
but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \
|
||||
@@ -597,7 +608,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
metaclass_of_class = metaclass1.name(self.db),
|
||||
metaclass_of_base = metaclass2.name(self.db),
|
||||
base = class2.name(self.db),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -654,11 +665,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
definition,
|
||||
);
|
||||
}
|
||||
DefinitionKind::Parameter(parameter) => {
|
||||
self.infer_parameter_definition(parameter, definition);
|
||||
DefinitionKind::VariadicPositionalParameter(parameter) => {
|
||||
self.infer_variadic_positional_parameter_definition(parameter, definition);
|
||||
}
|
||||
DefinitionKind::ParameterWithDefault(parameter_with_default) => {
|
||||
self.infer_parameter_with_default_definition(parameter_with_default, definition);
|
||||
DefinitionKind::VariadicKeywordParameter(parameter) => {
|
||||
self.infer_variadic_keyword_parameter_definition(parameter, definition);
|
||||
}
|
||||
DefinitionKind::Parameter(parameter_with_default) => {
|
||||
self.infer_parameter_definition(parameter_with_default, definition);
|
||||
}
|
||||
DefinitionKind::WithItem(with_item) => {
|
||||
self.infer_with_item_definition(
|
||||
@@ -732,9 +746,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&DIVISION_BY_ZERO,
|
||||
expr.into(),
|
||||
"division-by-zero",
|
||||
format_args!(
|
||||
"Cannot {op} object of type `{}` {by_zero}",
|
||||
left.display(self.db)
|
||||
@@ -757,9 +771,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO point out the conflicting declarations in the diagnostic?
|
||||
let symbol_table = self.index.symbol_table(binding.file_scope(self.db));
|
||||
let symbol_name = symbol_table.symbol(binding.symbol(self.db)).name();
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CONFLICTING_DECLARATIONS,
|
||||
node,
|
||||
"conflicting-declarations",
|
||||
format_args!(
|
||||
"Conflicting declared types for `{symbol_name}`: {}",
|
||||
conflicting.display(self.db)
|
||||
@@ -787,9 +801,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let ty = if inferred_ty.is_assignable_to(self.db, ty) {
|
||||
ty
|
||||
} else {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_DECLARATION,
|
||||
node,
|
||||
"invalid-declaration",
|
||||
format_args!(
|
||||
"Cannot declare type `{}` for inferred type `{}`",
|
||||
ty.display(self.db),
|
||||
@@ -871,6 +885,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) {
|
||||
// Parameters are odd: they are Definitions in the function body scope, but have no
|
||||
// constituent nodes that are part of the function body. In order to get diagnostics
|
||||
// merged/emitted for them, we need to explicitly infer their definitions here.
|
||||
for parameter in &function.parameters {
|
||||
self.infer_definition(parameter);
|
||||
}
|
||||
self.infer_body(&function.body);
|
||||
}
|
||||
|
||||
@@ -1033,33 +1053,126 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_parameter_with_default_definition(
|
||||
/// Set initial declared type (if annotated) and inferred type for a function-parameter symbol,
|
||||
/// in the function body scope.
|
||||
///
|
||||
/// The declared type is the annotated type, if any, or `Unknown`.
|
||||
///
|
||||
/// The inferred type is the annotated type, unioned with the type of the default value, if
|
||||
/// any. If both types are fully static, this union is a no-op (it should simplify to just the
|
||||
/// annotated type.) But in a case like `f(x=None)` with no annotated type, we want to infer
|
||||
/// the type `Unknown | None` for `x`, not just `Unknown`, so that we can error on usage of `x`
|
||||
/// that would not be valid for `None`.
|
||||
///
|
||||
/// If the default-value type is not assignable to the declared (annotated) type, we ignore the
|
||||
/// default-value type and just infer the annotated type; this is the same way we handle
|
||||
/// assignments, and allows an explicit annotation to override a bad inference.
|
||||
///
|
||||
/// Parameter definitions are odd in that they define a symbol in the function-body scope, so
|
||||
/// the Definition belongs to the function body scope, but the expressions (annotation and
|
||||
/// default value) both belong to outer scopes. (The default value always belongs to the outer
|
||||
/// scope in which the function is defined, the annotation belongs either to the outer scope,
|
||||
/// or maybe to an intervening type-params scope, if it's a generic function.) So we don't use
|
||||
/// `self.infer_expression` or store any expression types here, we just use `expression_ty` to
|
||||
/// get the types of the expressions from their respective scopes.
|
||||
///
|
||||
/// It is safe (non-cycle-causing) to use `expression_ty` here, because an outer scope can't
|
||||
/// depend on a definition from an inner scope, so we shouldn't be in-process of inferring the
|
||||
/// outer scope here.
|
||||
fn infer_parameter_definition(
|
||||
&mut self,
|
||||
parameter_with_default: &ast::ParameterWithDefault,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// TODO(dhruvmanila): Infer types from annotation or default expression
|
||||
// TODO check that default is assignable to parameter type
|
||||
self.infer_parameter_definition(¶meter_with_default.parameter, definition);
|
||||
let ast::ParameterWithDefault {
|
||||
parameter,
|
||||
default,
|
||||
range: _,
|
||||
} = parameter_with_default;
|
||||
let default_ty = default
|
||||
.as_ref()
|
||||
.map(|default| expression_ty(self.db, self.file, default));
|
||||
if let Some(annotation) = parameter.annotation.as_ref() {
|
||||
let declared_ty = expression_ty(self.db, self.file, annotation);
|
||||
let inferred_ty = if let Some(default_ty) = default_ty {
|
||||
if default_ty.is_assignable_to(self.db, declared_ty) {
|
||||
UnionType::from_elements(self.db, [declared_ty, default_ty])
|
||||
} else {
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_PARAMETER_DEFAULT,
|
||||
parameter_with_default.into(),
|
||||
format_args!(
|
||||
"Default value of type `{}` is not assignable to annotated parameter type `{}`",
|
||||
default_ty.display(self.db), declared_ty.display(self.db)),
|
||||
);
|
||||
declared_ty
|
||||
}
|
||||
} else {
|
||||
declared_ty
|
||||
};
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
declared_ty,
|
||||
inferred_ty,
|
||||
);
|
||||
} else {
|
||||
let ty = if let Some(default_ty) = default_ty {
|
||||
UnionType::from_elements(self.db, [Type::Unknown, default_ty])
|
||||
} else {
|
||||
Type::Unknown
|
||||
};
|
||||
self.add_binding(parameter.into(), definition, ty);
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_parameter_definition(
|
||||
/// Set initial declared/inferred types for a `*args` variadic positional parameter.
|
||||
///
|
||||
/// The annotated type is implicitly wrapped in a homogeneous tuple.
|
||||
///
|
||||
/// See `infer_parameter_definition` doc comment for some relevant observations about scopes.
|
||||
fn infer_variadic_positional_parameter_definition(
|
||||
&mut self,
|
||||
parameter: &ast::Parameter,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// TODO(dhruvmanila): Annotation expression is resolved at the enclosing scope, infer the
|
||||
// parameter type from there
|
||||
let annotated_ty = todo_type!("function parameter type");
|
||||
if parameter.annotation.is_some() {
|
||||
self.add_declaration_with_binding(
|
||||
if let Some(annotation) = parameter.annotation.as_ref() {
|
||||
let _annotated_ty = expression_ty(self.db, self.file, annotation);
|
||||
// TODO `tuple[annotated_ty, ...]`
|
||||
let ty = KnownClass::Tuple.to_instance(self.db);
|
||||
self.add_declaration_with_binding(parameter.into(), definition, ty, ty);
|
||||
} else {
|
||||
self.add_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
annotated_ty,
|
||||
annotated_ty,
|
||||
// TODO `tuple[Unknown, ...]`
|
||||
KnownClass::Tuple.to_instance(self.db),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set initial declared/inferred types for a `*args` variadic positional parameter.
|
||||
///
|
||||
/// The annotated type is implicitly wrapped in a string-keyed dictionary.
|
||||
///
|
||||
/// See `infer_parameter_definition` doc comment for some relevant observations about scopes.
|
||||
fn infer_variadic_keyword_parameter_definition(
|
||||
&mut self,
|
||||
parameter: &ast::Parameter,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
if let Some(annotation) = parameter.annotation.as_ref() {
|
||||
let _annotated_ty = expression_ty(self.db, self.file, annotation);
|
||||
// TODO `dict[str, annotated_ty]`
|
||||
let ty = KnownClass::Dict.to_instance(self.db);
|
||||
self.add_declaration_with_binding(parameter.into(), definition, ty, ty);
|
||||
} else {
|
||||
self.add_binding(parameter.into(), definition, annotated_ty);
|
||||
self.add_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
// TODO `dict[str, Unknown]`
|
||||
KnownClass::Dict.to_instance(self.db),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1297,9 +1410,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`).
|
||||
match (enter, exit) {
|
||||
(Symbol::Unbound, Symbol::Unbound) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`",
|
||||
context_expression_ty.display(self.db)
|
||||
@@ -1308,9 +1421,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Type::Unknown
|
||||
}
|
||||
(Symbol::Unbound, _) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`",
|
||||
context_expression_ty.display(self.db)
|
||||
@@ -1320,9 +1433,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
(Symbol::Type(enter_ty, enter_boundness), exit) => {
|
||||
if enter_boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
@@ -1334,9 +1447,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.call(self.db, &[context_expression_ty])
|
||||
.return_ty_result(self.db, context_expression.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!("
|
||||
Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable", context_expression = context_expression_ty.display(self.db), enter_ty = enter_ty.display(self.db)
|
||||
),
|
||||
@@ -1346,9 +1459,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
match exit {
|
||||
Symbol::Unbound => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`",
|
||||
context_expression_ty.display(self.db)
|
||||
@@ -1359,9 +1472,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO: Use the `exit_ty` to determine if any raised exception is suppressed.
|
||||
|
||||
if exit_boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
@@ -1386,9 +1499,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
@@ -1435,10 +1548,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Type::Tuple(tuple) => UnionType::from_elements(
|
||||
self.db,
|
||||
tuple.elements(self.db).iter().map(|ty| {
|
||||
ty.into_class_literal()
|
||||
.map_or(todo_type!(), |ClassLiteralType { class }| {
|
||||
Type::instance(class)
|
||||
})
|
||||
ty.into_class_literal().map_or(
|
||||
todo_type!("exception type"),
|
||||
|ClassLiteralType { class }| Type::instance(class),
|
||||
)
|
||||
}),
|
||||
),
|
||||
_ => todo_type!("exception type"),
|
||||
@@ -1466,9 +1579,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let bound_or_constraint = match bound.as_deref() {
|
||||
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
|
||||
if elts.len() < 2 {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_TYPE_VARIABLE_CONSTRAINTS,
|
||||
expr.into(),
|
||||
"invalid-typevar-constraints",
|
||||
format_args!("TypeVar must have at least two constrained types"),
|
||||
);
|
||||
self.infer_expression(expr);
|
||||
@@ -1789,9 +1902,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
assignment.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
|
||||
target_type.display(self.db),
|
||||
@@ -1810,9 +1923,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
let binary_return_ty = self.infer_binary_expression_type(left_ty, right_ty, op)
|
||||
.unwrap_or_else(|| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
assignment.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
|
||||
left_ty.display(self.db),
|
||||
@@ -1839,9 +1952,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
self.infer_binary_expression_type(left_ty, right_ty, op)
|
||||
.unwrap_or_else(|| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
assignment.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
|
||||
left_ty.display(self.db),
|
||||
@@ -1871,11 +1984,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
// Resolve the target type, assuming a load context.
|
||||
let target_type = match &**target {
|
||||
Expr::Name(name) => {
|
||||
ast::Expr::Name(name) => {
|
||||
self.store_expression_type(target, Type::Never);
|
||||
self.infer_name_load(name)
|
||||
}
|
||||
Expr::Attribute(attr) => {
|
||||
ast::Expr::Attribute(attr) => {
|
||||
self.store_expression_type(target, Type::Never);
|
||||
self.infer_attribute_load(attr)
|
||||
}
|
||||
@@ -2092,19 +2205,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
match module_ty.member(self.db, &ast::name::Name::new(&name.id)) {
|
||||
Symbol::Type(ty, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&POSSIBLY_UNBOUND_IMPORT,
|
||||
AnyNodeRef::Alias(alias),
|
||||
"possibly-unbound-import",
|
||||
format_args!("Member `{name}` of module `{module_name}` is possibly unbound",),
|
||||
format_args!("Member `{name}` of module `{module_name}` is possibly unbound", ),
|
||||
);
|
||||
}
|
||||
|
||||
ty
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNRESOLVED_IMPORT,
|
||||
AnyNodeRef::Alias(alias),
|
||||
"unresolved-import",
|
||||
format_args!("Module `{module_name}` has no member `{name}`",),
|
||||
);
|
||||
Type::Unknown
|
||||
@@ -2719,7 +2832,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics);
|
||||
|
||||
// TODO
|
||||
todo_type!()
|
||||
todo_type!("starred expression")
|
||||
}
|
||||
|
||||
fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> {
|
||||
@@ -2809,9 +2922,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
{
|
||||
let mut builtins_symbol = builtins_symbol(self.db, name);
|
||||
if builtins_symbol.is_unbound() && name == "reveal_type" {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNDEFINED_REVEAL,
|
||||
name_node.into(),
|
||||
"undefined-reveal",
|
||||
format_args!(
|
||||
"`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime"),
|
||||
);
|
||||
@@ -2906,9 +3019,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
match value_ty.member(self.db, &attr.id) {
|
||||
Symbol::Type(member_ty, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&POSSIBLY_UNBOUND_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
"possibly-unbound-attribute",
|
||||
format_args!(
|
||||
"Attribute `{}` on type `{}` is possibly unbound",
|
||||
attr.id,
|
||||
@@ -2920,9 +3033,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
member_ty
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNRESOLVED_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
"unresolved-attribute",
|
||||
format_args!(
|
||||
"Type `{}` has no attribute `{}`",
|
||||
value_ty.display(self.db),
|
||||
@@ -3001,9 +3114,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
unary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db),
|
||||
@@ -3013,9 +3126,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
unary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db),
|
||||
@@ -3054,9 +3167,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
self.infer_binary_expression_type(left_ty, right_ty, *op)
|
||||
.unwrap_or_else(|| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
binary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{op}` is unsupported between objects of type `{}` and `{}`",
|
||||
left_ty.display(self.db),
|
||||
@@ -3364,9 +3477,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_binary_type_comparison(left_ty, *op, right_ty)
|
||||
.unwrap_or_else(|error| {
|
||||
// Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome)
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
AnyNodeRef::ExprCompare(compare),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{}` is not supported for types `{}` and `{}`{}",
|
||||
error.op,
|
||||
@@ -4021,9 +4134,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Symbol::Unbound => {}
|
||||
Symbol::Type(dunder_getitem_method, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||
value_node.into(),
|
||||
"call-possibly-unbound-method",
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is possibly unbound",
|
||||
value_ty.display(self.db),
|
||||
@@ -4035,9 +4148,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node.into(),
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_ty().display(self.db),
|
||||
@@ -4065,9 +4178,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Symbol::Unbound => {}
|
||||
Symbol::Type(ty, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||
value_node.into(),
|
||||
"call-possibly-unbound-method",
|
||||
format_args!(
|
||||
"Method `__class_getitem__` of type `{}` is possibly unbound",
|
||||
value_ty.display(self.db),
|
||||
@@ -4079,9 +4192,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node.into(),
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_ty().display(self.db),
|
||||
@@ -4221,18 +4334,18 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
|
||||
|
||||
ast::Expr::BytesLiteral(bytes) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&BYTE_STRING_TYPE_ANNOTATION,
|
||||
bytes.into(),
|
||||
"annotation-byte-string",
|
||||
format_args!("Type expressions cannot use bytes literal"),
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
|
||||
ast::Expr::FString(fstring) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&FSTRING_TYPE_ANNOTATION,
|
||||
fstring.into(),
|
||||
"annotation-f-string",
|
||||
format_args!("Type expressions cannot use f-strings"),
|
||||
);
|
||||
self.infer_fstring_expression(fstring);
|
||||
@@ -4552,18 +4665,39 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
/// Given the slice of a `type[]` annotation, return the type that the annotation represents
|
||||
fn infer_subclass_of_type_expression(&mut self, slice: &ast::Expr) -> Type<'db> {
|
||||
match slice {
|
||||
ast::Expr::Name(_) => {
|
||||
ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
|
||||
let name_ty = self.infer_expression(slice);
|
||||
if let Some(ClassLiteralType { class }) = name_ty.into_class_literal() {
|
||||
Type::subclass_of(class)
|
||||
} else {
|
||||
todo_type!()
|
||||
todo_type!("unsupported type[X] special form")
|
||||
}
|
||||
}
|
||||
// TODO: attributes, unions, subscripts, etc.
|
||||
ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => {
|
||||
let union_ty = UnionType::from_elements(
|
||||
self.db,
|
||||
[
|
||||
self.infer_subclass_of_type_expression(&binary.left),
|
||||
self.infer_subclass_of_type_expression(&binary.right),
|
||||
],
|
||||
);
|
||||
self.store_expression_type(slice, union_ty);
|
||||
|
||||
union_ty
|
||||
}
|
||||
ast::Expr::Tuple(_) => {
|
||||
self.infer_type_expression(slice);
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
slice.into(),
|
||||
format_args!("type[...] must have exactly one type argument"),
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
// TODO: subscripts, etc.
|
||||
_ => {
|
||||
self.infer_type_expression(slice);
|
||||
todo_type!()
|
||||
todo_type!("unsupported type[X] special form")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4602,9 +4736,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Ok(ty) => ty,
|
||||
Err(nodes) => {
|
||||
for node in nodes {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_LITERAL_PARAMETER,
|
||||
node.into(),
|
||||
"invalid-literal-parameter",
|
||||
format_args!(
|
||||
"Type arguments for `Literal` must be `None`, \
|
||||
a literal value (int, bool, str, or bytes), or an enum value"
|
||||
@@ -4638,9 +4772,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
todo_type!("generic type alias")
|
||||
}
|
||||
KnownInstanceType::NoReturn | KnownInstanceType::Never => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_TYPE_PARAMETER,
|
||||
subscript.into(),
|
||||
"invalid-type-parameter",
|
||||
format_args!(
|
||||
"Type `{}` expected no type parameter",
|
||||
known_instance.repr(self.db)
|
||||
@@ -4649,9 +4783,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Type::Unknown
|
||||
}
|
||||
KnownInstanceType::LiteralString => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_TYPE_PARAMETER,
|
||||
subscript.into(),
|
||||
"invalid-type-parameter",
|
||||
format_args!(
|
||||
"Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?",
|
||||
known_instance.repr(self.db)
|
||||
@@ -5123,16 +5257,6 @@ mod tests {
|
||||
assert_diagnostic_messages(diagnostics, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_import_with_no_module_name() -> anyhow::Result<()> {
|
||||
// This test checks that invalid syntax in a `StmtImportFrom` node
|
||||
// leads to the type being inferred as `Unknown`
|
||||
let mut db = setup_db();
|
||||
db.write_file("src/foo.py", "from import bar")?;
|
||||
assert_public_ty(&db, "src/foo.py", "bar", "Unknown");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_method() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
@@ -5282,112 +5406,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_type() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/a.py",
|
||||
"
|
||||
w = b'red' b'knot'
|
||||
x = b'hello'
|
||||
y = b'world' + b'!'
|
||||
z = b'\\xff\\x00'
|
||||
",
|
||||
)?;
|
||||
|
||||
assert_public_ty(&db, "src/a.py", "w", "Literal[b\"redknot\"]");
|
||||
assert_public_ty(&db, "src/a.py", "x", "Literal[b\"hello\"]");
|
||||
assert_public_ty(&db, "src/a.py", "y", "Literal[b\"world!\"]");
|
||||
assert_public_ty(&db, "src/a.py", "z", "Literal[b\"\\xff\\x00\"]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ellipsis_type() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/a.py",
|
||||
"
|
||||
x = ...
|
||||
",
|
||||
)?;
|
||||
|
||||
// TODO: sys.version_info
|
||||
assert_public_ty(&db, "src/a.py", "x", "EllipsisType | ellipsis");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_cycle() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/a.py",
|
||||
"
|
||||
class A: pass
|
||||
import b
|
||||
class C(b.B): pass
|
||||
",
|
||||
)?;
|
||||
db.write_dedented(
|
||||
"src/b.py",
|
||||
"
|
||||
from a import A
|
||||
class B(A): pass
|
||||
",
|
||||
)?;
|
||||
|
||||
let a = system_path_to_file(&db, "src/a.py").expect("file to exist");
|
||||
let c_ty = global_symbol(&db, a, "C").expect_type();
|
||||
let c_class = c_ty.expect_class_literal().class;
|
||||
let mut c_mro = c_class.iter_mro(&db);
|
||||
let b_ty = c_mro.nth(1).unwrap();
|
||||
let b_class = b_ty.expect_class_base();
|
||||
assert_eq!(b_class.name(&db), "B");
|
||||
let mut b_mro = b_class.iter_mro(&db);
|
||||
let a_ty = b_mro.nth(1).unwrap();
|
||||
let a_class = a_ty.expect_class_base();
|
||||
assert_eq!(a_class.name(&db), "A");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An unbound function local that has definitions in the scope does not fall back to globals.
|
||||
#[test]
|
||||
fn unbound_function_local() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/a.py",
|
||||
"
|
||||
x = 1
|
||||
def f():
|
||||
y = x
|
||||
x = 2
|
||||
",
|
||||
)?;
|
||||
|
||||
let file = system_path_to_file(&db, "src/a.py").expect("file to exist");
|
||||
let index = semantic_index(&db, file);
|
||||
let function_scope = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.next()
|
||||
.unwrap()
|
||||
.0
|
||||
.to_scope_id(&db, file);
|
||||
let y_ty = symbol(&db, function_scope, "y").expect_type();
|
||||
let x_ty = symbol(&db, function_scope, "x").expect_type();
|
||||
|
||||
assert_eq!(y_ty.display(&db).to_string(), "Unknown");
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Literal[2]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A name reference to a never-defined symbol in a function is implicitly a global lookup.
|
||||
#[test]
|
||||
fn implicit_global_in_function() -> anyhow::Result<()> {
|
||||
@@ -5562,89 +5580,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonlocal_name_reference() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
y = x
|
||||
",
|
||||
)?;
|
||||
|
||||
assert_scope_ty(&db, "/src/a.py", &["f", "g"], "y", "Literal[1]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonlocal_name_reference_multi_level() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
def h():
|
||||
y = x
|
||||
",
|
||||
)?;
|
||||
|
||||
assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonlocal_name_reference_skips_class_scope() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
def f():
|
||||
x = 1
|
||||
class C:
|
||||
x = 2
|
||||
def g():
|
||||
y = x
|
||||
",
|
||||
)?;
|
||||
|
||||
assert_scope_ty(&db, "/src/a.py", &["f", "C", "g"], "y", "Literal[1]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonlocal_name_reference_skips_annotation_only_assignment() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
// it's pretty weird to have an annotated assignment in a function where the
|
||||
// name is otherwise not defined; maybe should be an error?
|
||||
x: int
|
||||
def h():
|
||||
y = x
|
||||
",
|
||||
)?;
|
||||
|
||||
assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_comprehension() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
@@ -328,15 +328,6 @@ impl<'db> ClassBase<'db> {
|
||||
Display { base: self, db }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
pub(super) fn expect_class_base(self) -> Class<'db> {
|
||||
match self {
|
||||
ClassBase::Class(class) => class,
|
||||
_ => panic!("Expected a `ClassBase::Class()` variant"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a `ClassBase` representing the class `builtins.object`
|
||||
fn object(db: &'db dyn Db) -> Self {
|
||||
KnownClass::Object
|
||||
|
||||
@@ -5,8 +5,127 @@ use ruff_python_ast::{self as ast, ModExpression, StringFlags};
|
||||
use ruff_python_parser::{parse_expression_range, Parsed};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::lint::{Level, LintStatus};
|
||||
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::Db;
|
||||
use crate::{declare_lint, Db};
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for f-strings in type annotation positions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Static analysis tools like Red Knot can't analyse type annotations that use f-string notation.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def test(): -> f"int":
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def test(): -> "int":
|
||||
/// ...
|
||||
/// ```
|
||||
pub(crate) static FSTRING_TYPE_ANNOTATION = {
|
||||
summary: "detects F-strings in type annotation positions",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for byte-strings in type annotation positions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Static analysis tools like Red Knot can't analyse type annotations that use byte-string notation.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def test(): -> b"int":
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def test(): -> "int":
|
||||
/// ...
|
||||
/// ```
|
||||
pub(crate) static BYTE_STRING_TYPE_ANNOTATION = {
|
||||
summary: "detects byte strings in type annotation positions",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for raw-strings in type annotation positions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Static analysis tools like Red Knot can't analyse type annotations that use raw-string notation.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def test(): -> r"int":
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def test(): -> "int":
|
||||
/// ...
|
||||
/// ```
|
||||
pub(crate) static RAW_STRING_TYPE_ANNOTATION = {
|
||||
summary: "detects raw strings in type annotation positions",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for implicit concatenated strings in type annotation positions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Static analysis tools like Red Knot can't analyse type annotations that use implicit concatenated strings.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def test(): -> "Literal[" "5" "]":
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def test(): -> "Literal[5]":
|
||||
/// ...
|
||||
/// ```
|
||||
pub(crate) static IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION = {
|
||||
summary: "detects implicit concatenated strings in type annotations",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static INVALID_SYNTAX_IN_FORWARD_ANNOTATION = {
|
||||
summary: "detects invalid syntax in forward annotations",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO
|
||||
pub(crate) static ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION = {
|
||||
summary: "detects forward type annotations with escape characters",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
type AnnotationParseResult = Result<Parsed<ModExpression>, TypeCheckDiagnostics>;
|
||||
|
||||
@@ -25,9 +144,9 @@ pub(crate) fn parse_string_annotation(
|
||||
if let [string_literal] = string_expr.value.as_slice() {
|
||||
let prefix = string_literal.flags.prefix();
|
||||
if prefix.is_raw() {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&RAW_STRING_TYPE_ANNOTATION,
|
||||
string_literal.into(),
|
||||
"annotation-raw-string",
|
||||
format_args!("Type expressions cannot use raw string literal"),
|
||||
);
|
||||
// Compare the raw contents (without quotes) of the expression with the parsed contents
|
||||
@@ -49,26 +168,26 @@ pub(crate) fn parse_string_annotation(
|
||||
// ```
|
||||
match parse_expression_range(source.as_str(), range_excluding_quotes) {
|
||||
Ok(parsed) => return Ok(parsed),
|
||||
Err(parse_error) => diagnostics.add(
|
||||
Err(parse_error) => diagnostics.add_lint(
|
||||
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
string_literal.into(),
|
||||
"forward-annotation-syntax-error",
|
||||
format_args!("Syntax error in forward annotation: {}", parse_error.error),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
// The raw contents of the string doesn't match the parsed content. This could be the
|
||||
// case for annotations that contain escape sequences.
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION,
|
||||
string_expr.into(),
|
||||
"annotation-escape-character",
|
||||
format_args!("Type expressions cannot contain escape characters"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// String is implicitly concatenated.
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
|
||||
string_expr.into(),
|
||||
"annotation-implicit-concat",
|
||||
format_args!("Type expressions cannot span multiple string literals"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dhruvmanila): Publish diagnostics if the client doesnt support pull diagnostics
|
||||
// TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -86,14 +86,15 @@ fn to_lsp_diagnostic(
|
||||
|
||||
let severity = match diagnostic.severity() {
|
||||
Severity::Info => DiagnosticSeverity::INFORMATION,
|
||||
Severity::Error => DiagnosticSeverity::ERROR,
|
||||
Severity::Warning => DiagnosticSeverity::WARNING,
|
||||
Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR,
|
||||
};
|
||||
|
||||
Diagnostic {
|
||||
range,
|
||||
severity: Some(severity),
|
||||
tags: None,
|
||||
code: Some(NumberOrString::String(diagnostic.rule().to_string())),
|
||||
code: Some(NumberOrString::String(diagnostic.id().to_string())),
|
||||
code_description: None,
|
||||
source: Some("red-knot".into()),
|
||||
message: diagnostic.message().into_owned(),
|
||||
|
||||
@@ -27,6 +27,8 @@ regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
|
||||
@@ -225,6 +225,22 @@ A header-demarcated section must either be a test or a grouping header; it canno
|
||||
a header section can either contain embedded files (making it a test), or it can contain more
|
||||
deeply-nested headers (headers with more `#`), but it cannot contain both.
|
||||
|
||||
## Configuration
|
||||
|
||||
The test framework supports a TOML-based configuration format, which is a subset of the full red-knot
|
||||
configuration format. This configuration can be specified in fenced code blocks with `toml` as the
|
||||
language tag:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.10"
|
||||
```
|
||||
````
|
||||
|
||||
This configuration will apply to all tests in the same section, and all nested sections within that
|
||||
section. Nested sections can override configurations from their parent sections.
|
||||
|
||||
## Documentation of tests
|
||||
|
||||
Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by
|
||||
@@ -282,30 +298,6 @@ possible in these files.
|
||||
|
||||
A fenced code block with no language will always be an error.
|
||||
|
||||
### Configuration
|
||||
|
||||
We will add the ability to specify non-default red-knot configurations to use in tests, by including
|
||||
a TOML code block:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[tool.knot]
|
||||
warn-on-any = true
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(x: Any): # error: [use-of-any]
|
||||
pass
|
||||
```
|
||||
````
|
||||
|
||||
It should be possible to include a TOML code block in a single test (as shown), or in a grouping
|
||||
section, in which case it applies to all nested tests within that grouping section. Configurations
|
||||
at multiple level are allowed and merged, with the most-nested (closest to the test) taking
|
||||
precedence.
|
||||
|
||||
### Running just a single test from a suite
|
||||
|
||||
Having each test in a suite always run as a distinct Rust test would require writing our own test
|
||||
@@ -317,11 +309,11 @@ variable.
|
||||
|
||||
### Configuring search paths and kinds
|
||||
|
||||
The red-knot TOML configuration format hasn't been designed yet, and we may want to implement
|
||||
The red-knot TOML configuration format hasn't been finalized, and we may want to implement
|
||||
support in the test framework for configuring search paths before it is designed. If so, we can
|
||||
define some configuration options for now under the `[tool.knot.tests]` namespace. In the future,
|
||||
perhaps some of these can be replaced by real red-knot configuration options; some or all may also
|
||||
be kept long-term as test-specific options.
|
||||
define some configuration options for now under the `[tests]` namespace. In the future, perhaps
|
||||
some of these can be replaced by real red-knot configuration options; some or all may also be
|
||||
kept long-term as test-specific options.
|
||||
|
||||
Some configuration options we will want to provide:
|
||||
|
||||
@@ -339,13 +331,13 @@ non-default value using the `workspace-root` config.
|
||||
|
||||
### Specifying a custom typeshed
|
||||
|
||||
Some tests will need to override the default typeshed with custom files. The `[tool.knot.tests]`
|
||||
configuration option `typeshed-root` should be usable for this:
|
||||
Some tests will need to override the default typeshed with custom files. The `[environment]`
|
||||
configuration option `typeshed-path` can be used to do this:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[tool.knot.tests]
|
||||
typeshed-root = "/typeshed"
|
||||
[environment]
|
||||
typeshed-path = "/typeshed"
|
||||
```
|
||||
|
||||
This file is importable as part of our custom typeshed, because it is within `/typeshed`, which we
|
||||
|
||||
28
crates/red_knot_test/src/config.rs
Normal file
28
crates/red_knot_test/src/config.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! TOML-deserializable Red Knot configuration, similar to `knot.toml`, to be able to
|
||||
//! control some configuration options from Markdown files. For now, this supports the
|
||||
//! following limited structure:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [environment]
|
||||
//! target-version = "3.10"
|
||||
//! ```
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct MarkdownTestConfig {
|
||||
pub(crate) environment: Environment,
|
||||
}
|
||||
|
||||
impl MarkdownTestConfig {
|
||||
pub(crate) fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||
toml::from_str(s).context("Error while parsing Markdown TOML config")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct Environment {
|
||||
#[serde(rename = "target-version")]
|
||||
pub(crate) target_version: String,
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
use red_knot_python_semantic::{
|
||||
Db as SemanticDb, Program, ProgramSettings, PythonVersion, SearchPathSettings,
|
||||
default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonVersion,
|
||||
SearchPathSettings,
|
||||
};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem};
|
||||
@@ -13,16 +15,20 @@ pub(crate) struct Db {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
rule_selection: RuleSelection,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
|
||||
let rule_selection = RuleSelection::from_registry(&default_lint_registry());
|
||||
|
||||
let db = Self {
|
||||
workspace_root,
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
rule_selection,
|
||||
};
|
||||
|
||||
db.memory_file_system()
|
||||
@@ -85,6 +91,10 @@ impl SemanticDb for Db {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -144,7 +144,7 @@ struct DiagnosticWithLine<T> {
|
||||
mod tests {
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
@@ -190,8 +190,8 @@ mod tests {
|
||||
}
|
||||
|
||||
impl Diagnostic for DummyDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"dummy"
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Lint(LintName::of("dummy"))
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
|
||||
@@ -2,14 +2,17 @@ use camino::Utf8Path;
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::Program;
|
||||
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
|
||||
use ruff_db::files::{system_path_to_file, File, Files};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_source_file::LineIndex;
|
||||
use ruff_text_size::TextSize;
|
||||
use salsa::Setter;
|
||||
|
||||
mod assertion;
|
||||
mod config;
|
||||
mod db;
|
||||
mod diagnostic;
|
||||
mod matcher;
|
||||
@@ -26,7 +29,7 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
let suite = match test_parser::parse(short_title, &source) {
|
||||
Ok(suite) => suite,
|
||||
Err(err) => {
|
||||
panic!("Error parsing `{path}`: {err}")
|
||||
panic!("Error parsing `{path}`: {err:?}")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,6 +42,10 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
continue;
|
||||
}
|
||||
|
||||
Program::get(&db)
|
||||
.set_target_version(&mut db)
|
||||
.to(test.target_version());
|
||||
|
||||
// Remove all files so that the db is in a "fresh" state.
|
||||
db.memory_file_system().remove_all();
|
||||
Files::sync_all(&mut db);
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::SortedDiagnostics;
|
||||
use colored::Colorize;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
@@ -146,7 +146,7 @@ fn maybe_add_undefined_reveal_clarification<T: Diagnostic>(
|
||||
diagnostic: &T,
|
||||
original: std::fmt::Arguments,
|
||||
) -> String {
|
||||
if diagnostic.rule() == "undefined-reveal" {
|
||||
if diagnostic.id().is_lint_named("undefined-reveal") {
|
||||
format!(
|
||||
"{} add a `# revealed` assertion on this line (original diagnostic: {original})",
|
||||
"used built-in `reveal_type`:".yellow()
|
||||
@@ -163,7 +163,7 @@ where
|
||||
fn unmatched(&self) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
self,
|
||||
format_args!(r#"[{}] "{}""#, self.rule(), self.message()),
|
||||
format_args!(r#"[{}] "{}""#, self.id(), self.message()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ where
|
||||
fn unmatched_with_column(&self, column: OneIndexed) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
self,
|
||||
format_args!(r#"{column} [{}] "{}""#, self.rule(), self.message()),
|
||||
format_args!(r#"{column} [{}] "{}""#, self.id(), self.message()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -270,10 +270,11 @@ impl Matcher {
|
||||
match assertion {
|
||||
Assertion::Error(error) => {
|
||||
let position = unmatched.iter().position(|diagnostic| {
|
||||
!error.rule.is_some_and(|rule| rule != diagnostic.rule())
|
||||
&& !error
|
||||
.column
|
||||
.is_some_and(|col| col != self.column(*diagnostic))
|
||||
!error.rule.is_some_and(|rule| {
|
||||
!(diagnostic.id().is_lint_named(rule) || diagnostic.id().matches(rule))
|
||||
}) && !error
|
||||
.column
|
||||
.is_some_and(|col| col != self.column(*diagnostic))
|
||||
&& !error
|
||||
.message_contains
|
||||
.is_some_and(|needle| !diagnostic.message().contains(needle))
|
||||
@@ -294,12 +295,12 @@ impl Matcher {
|
||||
let expected_reveal_type_message = format!("Revealed type is `{expected_type}`");
|
||||
for (index, diagnostic) in unmatched.iter().enumerate() {
|
||||
if matched_revealed_type.is_none()
|
||||
&& diagnostic.rule() == "revealed-type"
|
||||
&& diagnostic.id() == DiagnosticId::RevealedType
|
||||
&& diagnostic.message() == expected_reveal_type_message
|
||||
{
|
||||
matched_revealed_type = Some(index);
|
||||
} else if matched_undefined_reveal.is_none()
|
||||
&& diagnostic.rule() == "undefined-reveal"
|
||||
&& diagnostic.id().is_lint_named("undefined-reveal")
|
||||
{
|
||||
matched_undefined_reveal = Some(index);
|
||||
}
|
||||
@@ -323,7 +324,7 @@ impl Matcher {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FailuresByLine;
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
@@ -332,16 +333,16 @@ mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
struct ExpectedDiagnostic {
|
||||
rule: &'static str,
|
||||
id: DiagnosticId,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl ExpectedDiagnostic {
|
||||
fn new(rule: &'static str, message: &'static str, offset: usize) -> Self {
|
||||
fn new(id: DiagnosticId, message: &'static str, offset: usize) -> Self {
|
||||
let offset: u32 = offset.try_into().unwrap();
|
||||
Self {
|
||||
rule,
|
||||
id,
|
||||
message,
|
||||
range: TextRange::new(offset.into(), (offset + 1).into()),
|
||||
}
|
||||
@@ -349,7 +350,7 @@ mod tests {
|
||||
|
||||
fn into_diagnostic(self, file: File) -> TestDiagnostic {
|
||||
TestDiagnostic {
|
||||
rule: self.rule,
|
||||
id: self.id,
|
||||
message: self.message,
|
||||
range: self.range,
|
||||
file,
|
||||
@@ -359,15 +360,15 @@ mod tests {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestDiagnostic {
|
||||
rule: &'static str,
|
||||
id: DiagnosticId,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl Diagnostic for TestDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
self.rule
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
@@ -437,7 +438,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"revealed-type",
|
||||
DiagnosticId::RevealedType,
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
@@ -451,7 +452,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"not-revealed-type",
|
||||
DiagnosticId::lint("not-revealed-type"),
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
@@ -463,7 +464,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [not-revealed-type] "Revealed type is `Foo`""#,
|
||||
r#"unexpected error: 1 [lint/not-revealed-type] "Revealed type is `Foo`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
@@ -474,7 +475,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"revealed-type",
|
||||
DiagnosticId::RevealedType,
|
||||
"Something else",
|
||||
0,
|
||||
)],
|
||||
@@ -504,8 +505,12 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "Revealed type is `Foo`", 0),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("undefined-reveal"),
|
||||
"Doesn't matter",
|
||||
0,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -517,7 +522,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"undefined-reveal",
|
||||
DiagnosticId::lint("undefined-reveal"),
|
||||
"Doesn't matter",
|
||||
0,
|
||||
)],
|
||||
@@ -531,8 +536,12 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "Revealed type is `Bar`", 0),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("undefined-reveal"),
|
||||
"Doesn't matter",
|
||||
0,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -553,8 +562,16 @@ mod tests {
|
||||
let result = get_result(
|
||||
"reveal_type(1)",
|
||||
vec![
|
||||
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("undefined-reveal"),
|
||||
"undefined reveal message",
|
||||
0,
|
||||
),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::RevealedType,
|
||||
"Revealed type is `Literal[1]`",
|
||||
12,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -564,7 +581,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
|
||||
original diagnostic: [undefined-reveal] \"undefined reveal message\")",
|
||||
original diagnostic: [lint/undefined-reveal] \"undefined reveal message\")",
|
||||
r#"unexpected error: [revealed-type] "Revealed type is `Literal[1]`""#,
|
||||
],
|
||||
)],
|
||||
@@ -576,8 +593,16 @@ mod tests {
|
||||
let result = get_result(
|
||||
"reveal_type(1) # error: [something-else]",
|
||||
vec![
|
||||
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("undefined-reveal"),
|
||||
"undefined reveal message",
|
||||
0,
|
||||
),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::RevealedType,
|
||||
"Revealed type is `Literal[1]`",
|
||||
12,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -588,7 +613,7 @@ mod tests {
|
||||
&[
|
||||
"unmatched assertion: error: [something-else]",
|
||||
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
|
||||
original diagnostic: 1 [undefined-reveal] \"undefined reveal message\")",
|
||||
original diagnostic: 1 [lint/undefined-reveal] \"undefined reveal message\")",
|
||||
r#"unexpected error: 13 [revealed-type] "Revealed type is `Literal[1]`""#,
|
||||
],
|
||||
)],
|
||||
@@ -606,7 +631,11 @@ mod tests {
|
||||
fn error_match_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -616,7 +645,11 @@ mod tests {
|
||||
fn error_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("anything"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -625,7 +658,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: [some-rule]",
|
||||
r#"unexpected error: 1 [anything] "Any message""#,
|
||||
r#"unexpected error: 1 [lint/anything] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
@@ -636,7 +669,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"anything",
|
||||
DiagnosticId::lint("anything"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -649,7 +682,11 @@ mod tests {
|
||||
fn error_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("anything"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -658,7 +695,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: "contains this""#,
|
||||
r#"unexpected error: 1 [anything] "Any message""#,
|
||||
r#"unexpected error: 1 [lint/anything] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
@@ -668,7 +705,11 @@ mod tests {
|
||||
fn error_match_column_and_rule() {
|
||||
let result = get_result(
|
||||
"x # error: 1 [some-rule]",
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -678,7 +719,11 @@ mod tests {
|
||||
fn error_wrong_column() {
|
||||
let result = get_result(
|
||||
"x # error: 2 [rule]",
|
||||
vec![ExpectedDiagnostic::new("rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("rule"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -687,7 +732,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: 2 [rule]",
|
||||
r#"unexpected error: 1 [rule] "Any message""#,
|
||||
r#"unexpected error: 1 [lint/rule] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
@@ -698,7 +743,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"anything",
|
||||
DiagnosticId::lint("anything"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -712,7 +757,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: [a-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"a-rule",
|
||||
DiagnosticId::lint("a-rule"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -726,7 +771,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [a-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"a-rule",
|
||||
DiagnosticId::lint("a-rule"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -740,7 +785,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: 2 [some-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"some-rule",
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -752,7 +797,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 2 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [some-rule] "message contains this""#,
|
||||
r#"unexpected error: 1 [lint/some-rule] "message contains this""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
@@ -763,7 +808,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"other-rule",
|
||||
DiagnosticId::lint("other-rule"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -775,7 +820,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [other-rule] "message contains this""#,
|
||||
r#"unexpected error: 1 [lint/other-rule] "message contains this""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
@@ -785,7 +830,11 @@ mod tests {
|
||||
fn error_match_all_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -794,7 +843,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [some-rule] "Any message""#,
|
||||
r#"unexpected error: 1 [lint/some-rule] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
@@ -818,9 +867,9 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("line-two", "msg", two),
|
||||
ExpectedDiagnostic::new("line-three", "msg", three),
|
||||
ExpectedDiagnostic::new("line-five", "msg", five),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-three"), "msg", three),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-five"), "msg", five),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -828,9 +877,9 @@ mod tests {
|
||||
result,
|
||||
&[
|
||||
(1, &["unmatched assertion: error: [line-one]"]),
|
||||
(2, &[r#"unexpected error: [line-two] "msg""#]),
|
||||
(2, &[r#"unexpected error: [lint/line-two] "msg""#]),
|
||||
(4, &["unmatched assertion: error: [line-four]"]),
|
||||
(5, &[r#"unexpected error: [line-five] "msg""#]),
|
||||
(5, &[r#"unexpected error: [lint/line-five] "msg""#]),
|
||||
(6, &["unmatched assertion: error: [line-six]"]),
|
||||
],
|
||||
);
|
||||
@@ -849,12 +898,15 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("line-one", "msg", one),
|
||||
ExpectedDiagnostic::new("line-two", "msg", two),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-one"), "msg", one),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(2, &[r#"unexpected error: [line-two] "msg""#])]);
|
||||
assert_fail(
|
||||
result,
|
||||
&[(2, &[r#"unexpected error: [lint/line-two] "msg""#])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -870,8 +922,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("other-rule", "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -891,8 +943,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -912,15 +964,15 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("other-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("third-rule", "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("third-rule"), "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(3, &[r#"unexpected error: 1 [third-rule] "msg""#])],
|
||||
&[(3, &[r#"unexpected error: 1 [lint/third-rule] "msg""#])],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -938,8 +990,12 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("undefined-reveal"), "msg", reveal),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::RevealedType,
|
||||
"Revealed type is `Literal[5]`",
|
||||
reveal,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -952,7 +1008,11 @@ mod tests {
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"some message",
|
||||
x,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -961,7 +1021,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
r#"unexpected error: 1 [lint/some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
@@ -973,7 +1033,11 @@ mod tests {
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"some message",
|
||||
x,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -982,7 +1046,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
r#"unexpected error: 1 [lint/some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use memchr::memchr2;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use regex::{Captures, Match, Regex};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
@@ -8,6 +10,8 @@ use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_trivia::Cursor;
|
||||
use ruff_text_size::{TextLen, TextSize};
|
||||
|
||||
use crate::config::MarkdownTestConfig;
|
||||
|
||||
/// Parse the Markdown `source` as a test suite with given `title`.
|
||||
pub(crate) fn parse<'s>(title: &'s str, source: &'s str) -> anyhow::Result<MarkdownTestSuite<'s>> {
|
||||
let parser = Parser::new(title, source);
|
||||
@@ -69,6 +73,10 @@ impl<'m, 's> MarkdownTest<'m, 's> {
|
||||
pub(crate) fn files(&self) -> impl Iterator<Item = &'m EmbeddedFile<'s>> {
|
||||
self.files.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn target_version(&self) -> PythonVersion {
|
||||
self.section.target_version
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator yielding all [`MarkdownTest`]s in a [`MarkdownTestSuite`].
|
||||
@@ -117,6 +125,7 @@ struct Section<'s> {
|
||||
title: &'s str,
|
||||
level: u8,
|
||||
parent_id: Option<SectionId>,
|
||||
target_version: PythonVersion,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
@@ -174,7 +183,7 @@ impl SectionStack {
|
||||
popped
|
||||
}
|
||||
|
||||
fn parent(&mut self) -> SectionId {
|
||||
fn top(&mut self) -> SectionId {
|
||||
*self
|
||||
.0
|
||||
.last()
|
||||
@@ -201,6 +210,9 @@ struct Parser<'s> {
|
||||
|
||||
/// Names of embedded files in current active section.
|
||||
current_section_files: Option<FxHashSet<&'s str>>,
|
||||
|
||||
/// Whether or not the current section has a config block.
|
||||
current_section_has_config: bool,
|
||||
}
|
||||
|
||||
impl<'s> Parser<'s> {
|
||||
@@ -210,6 +222,7 @@ impl<'s> Parser<'s> {
|
||||
title,
|
||||
level: 0,
|
||||
parent_id: None,
|
||||
target_version: PythonVersion::default(),
|
||||
});
|
||||
Self {
|
||||
sections,
|
||||
@@ -218,6 +231,7 @@ impl<'s> Parser<'s> {
|
||||
source_len: source.text_len(),
|
||||
stack: SectionStack::new(root_section_id),
|
||||
current_section_files: None,
|
||||
current_section_has_config: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,12 +299,13 @@ impl<'s> Parser<'s> {
|
||||
|
||||
self.pop_sections_to_level(header_level);
|
||||
|
||||
let parent = self.stack.parent();
|
||||
let parent = self.stack.top();
|
||||
|
||||
let section = Section {
|
||||
title,
|
||||
level: header_level.try_into()?,
|
||||
parent_id: Some(parent),
|
||||
target_version: self.sections[parent].target_version,
|
||||
};
|
||||
|
||||
if self.current_section_files.is_some() {
|
||||
@@ -305,13 +320,14 @@ impl<'s> Parser<'s> {
|
||||
self.stack.push(section_id);
|
||||
|
||||
self.current_section_files = None;
|
||||
self.current_section_has_config = false;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_code_block(&mut self, captures: &Captures<'s>) -> anyhow::Result<()> {
|
||||
// We never pop the implicit root section.
|
||||
let parent = self.stack.parent();
|
||||
let section = self.stack.top();
|
||||
|
||||
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default();
|
||||
|
||||
@@ -333,16 +349,24 @@ impl<'s> Parser<'s> {
|
||||
|
||||
let path = config.get("path").copied().unwrap_or("test.py");
|
||||
|
||||
// CODE_RE can't match without matches for 'lang' and 'code'.
|
||||
let lang = captures
|
||||
.name("lang")
|
||||
.as_ref()
|
||||
.map(Match::as_str)
|
||||
.unwrap_or_default();
|
||||
let code = captures.name("code").unwrap().into();
|
||||
|
||||
if lang == "toml" {
|
||||
return self.parse_config(code);
|
||||
}
|
||||
|
||||
self.files.push(EmbeddedFile {
|
||||
path,
|
||||
section: parent,
|
||||
lang: captures
|
||||
.name("lang")
|
||||
.as_ref()
|
||||
.map(Match::as_str)
|
||||
.unwrap_or_default(),
|
||||
// CODE_RE can't match without matches for 'lang' and 'code'.
|
||||
code: captures.name("code").unwrap().into(),
|
||||
section,
|
||||
lang,
|
||||
|
||||
code,
|
||||
|
||||
md_offset: self.offset(),
|
||||
});
|
||||
@@ -354,12 +378,12 @@ impl<'s> Parser<'s> {
|
||||
"Test `{}` has duplicate files named `{path}`. \
|
||||
(This is the default filename; \
|
||||
consider giving some files an explicit name with `path=...`.)",
|
||||
self.sections[parent].title
|
||||
self.sections[section].title
|
||||
));
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"Test `{}` has duplicate files named `{path}`.",
|
||||
self.sections[parent].title
|
||||
self.sections[section].title
|
||||
));
|
||||
};
|
||||
} else {
|
||||
@@ -369,8 +393,36 @@ impl<'s> Parser<'s> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_config(&mut self, code: &str) -> anyhow::Result<()> {
|
||||
if self.current_section_has_config {
|
||||
bail!("Multiple TOML configuration blocks in the same section are not allowed.");
|
||||
}
|
||||
|
||||
let config = MarkdownTestConfig::from_str(code)?;
|
||||
let target_version = config.environment.target_version;
|
||||
|
||||
let parts = target_version
|
||||
.split('.')
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context(format!(
|
||||
"Invalid 'target-version' component: '{target_version}'"
|
||||
))?;
|
||||
|
||||
if parts.len() != 2 {
|
||||
bail!("Invalid 'target-version': expected MAJOR.MINOR, got '{target_version}'.",);
|
||||
}
|
||||
|
||||
let current_section = &mut self.sections[self.stack.top()];
|
||||
current_section.target_version = PythonVersion::from((parts[0], parts[1]));
|
||||
|
||||
self.current_section_has_config = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pop_sections_to_level(&mut self, level: usize) {
|
||||
while level <= self.sections[self.stack.parent()].level.into() {
|
||||
while level <= self.sections[self.stack.top()].level.into() {
|
||||
self.stack.pop();
|
||||
// We would have errored before pushing a child section if there were files, so we know
|
||||
// no parent section can have files.
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use std::panic::RefUnwindSafe;
|
||||
use std::sync::Arc;
|
||||
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Cancelled, Event};
|
||||
|
||||
use crate::workspace::{check_file, Workspace, WorkspaceMetadata};
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Cancelled, Event};
|
||||
|
||||
mod changes;
|
||||
|
||||
@@ -25,6 +26,7 @@ pub struct RootDatabase {
|
||||
storage: salsa::Storage<RootDatabase>,
|
||||
files: Files,
|
||||
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
impl RootDatabase {
|
||||
@@ -32,11 +34,14 @@ impl RootDatabase {
|
||||
where
|
||||
S: System + 'static + Send + Sync + RefUnwindSafe,
|
||||
{
|
||||
let rule_selection = RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY);
|
||||
|
||||
let mut db = Self {
|
||||
workspace: None,
|
||||
storage: salsa::Storage::default(),
|
||||
files: Files::default(),
|
||||
system: Arc::new(system),
|
||||
rule_selection: Arc::new(rule_selection),
|
||||
};
|
||||
|
||||
// Initialize the `Program` singleton
|
||||
@@ -83,6 +88,7 @@ impl RootDatabase {
|
||||
storage: self.storage.clone(),
|
||||
files: self.files.snapshot(),
|
||||
system: Arc::clone(&self.system),
|
||||
rule_selection: Arc::clone(&self.rule_selection),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +122,10 @@ impl SemanticDb for RootDatabase {
|
||||
|
||||
workspace.is_file_open(self, file)
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
@@ -162,6 +172,7 @@ pub(crate) mod tests {
|
||||
|
||||
use salsa::Event;
|
||||
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
use red_knot_python_semantic::Db as SemanticDb;
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
@@ -170,14 +181,16 @@ pub(crate) mod tests {
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::workspace::{Workspace, WorkspaceMetadata};
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Arc<std::sync::Mutex<Vec<Event>>>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
rule_selection: RuleSelection,
|
||||
workspace: Option<Workspace>,
|
||||
}
|
||||
|
||||
@@ -189,6 +202,7 @@ pub(crate) mod tests {
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
events: Arc::default(),
|
||||
rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),
|
||||
workspace: None,
|
||||
};
|
||||
|
||||
@@ -259,6 +273,10 @@ pub(crate) mod tests {
|
||||
fn is_file_open(&self, file: ruff_db::files::File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder};
|
||||
use red_knot_python_semantic::register_semantic_lints;
|
||||
|
||||
pub mod db;
|
||||
pub mod watch;
|
||||
pub mod workspace;
|
||||
|
||||
pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock<LintRegistry> =
|
||||
std::sync::LazyLock::new(default_lints_registry);
|
||||
|
||||
pub fn default_lints_registry() -> LintRegistry {
|
||||
let mut builder = LintRegistryBuilder::default();
|
||||
register_semantic_lints(&mut builder);
|
||||
builder.build()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
|
||||
pub use metadata::{PackageMetadata, WorkspaceDiscoveryError, WorkspaceMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::SearchPathSettings;
|
||||
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic, Severity};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{source_text, SourceTextError};
|
||||
use ruff_db::system::FileType;
|
||||
@@ -533,8 +533,8 @@ pub struct IOErrorDiagnostic {
|
||||
}
|
||||
|
||||
impl Diagnostic for IOErrorDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"io"
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Io
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
|
||||
@@ -694,7 +694,7 @@ mod tests {
|
||||
/// Folders that match the members pattern but don't have a pyproject.toml
|
||||
/// aren't valid members and discovery fails. However, don't fail
|
||||
/// if the folder name indicates that it is a hidden folder that might
|
||||
/// have been crated by another tool
|
||||
/// have been created by another tool
|
||||
#[test]
|
||||
fn member_pattern_matching_hidden_folder() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
|
||||
@@ -64,11 +64,19 @@ enum ChangeKind {
|
||||
/// Return the [`ChangeKind`] based on the list of modified file paths.
|
||||
///
|
||||
/// Returns `None` if no relevant changes were detected.
|
||||
fn change_detected(paths: &[PathBuf]) -> Option<ChangeKind> {
|
||||
fn change_detected(event: ¬ify::Event) -> Option<ChangeKind> {
|
||||
// If any `.toml` files were modified, return `ChangeKind::Configuration`. Otherwise, return
|
||||
// `ChangeKind::SourceFile` if any `.py`, `.pyi`, `.pyw`, or `.ipynb` files were modified.
|
||||
let mut source_file = false;
|
||||
for path in paths {
|
||||
|
||||
if event.kind.is_access() || event.kind.is_other() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if event.need_rescan() {
|
||||
return Some(ChangeKind::Configuration);
|
||||
}
|
||||
for path in &event.paths {
|
||||
if let Some(suffix) = path.extension() {
|
||||
match suffix.to_str() {
|
||||
Some("toml") => {
|
||||
@@ -377,7 +385,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
let Some(change_kind) = change_detected(&event?.paths) else {
|
||||
let Some(change_kind) = change_detected(&event?) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -483,73 +491,113 @@ mod test_file_change_detector {
|
||||
fn detect_correct_file_change() {
|
||||
assert_eq!(
|
||||
Some(ChangeKind::Configuration),
|
||||
change_detected(&[
|
||||
PathBuf::from("tmp/pyproject.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("tmp/pyproject.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(ChangeKind::Configuration),
|
||||
change_detected(&[
|
||||
PathBuf::from("pyproject.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("pyproject.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(ChangeKind::Configuration),
|
||||
change_detected(&[
|
||||
PathBuf::from("tmp1/tmp2/tmp3/pyproject.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("tmp1/tmp2/tmp3/pyproject.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(ChangeKind::Configuration),
|
||||
change_detected(&[
|
||||
PathBuf::from("tmp/ruff.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("tmp/ruff.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(ChangeKind::Configuration),
|
||||
change_detected(&[
|
||||
PathBuf::from("tmp/.ruff.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("tmp/.ruff.toml"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(ChangeKind::SourceFile),
|
||||
change_detected(&[
|
||||
PathBuf::from("tmp/rule.py"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("tmp/rule.py"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(ChangeKind::SourceFile),
|
||||
change_detected(&[
|
||||
PathBuf::from("tmp/rule.pyi"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("tmp/rule.pyi"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(ChangeKind::Configuration),
|
||||
change_detected(&[
|
||||
PathBuf::from("pyproject.toml"),
|
||||
PathBuf::from("tmp/rule.py"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("pyproject.toml"),
|
||||
PathBuf::from("tmp/rule.py"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(ChangeKind::Configuration),
|
||||
change_detected(&[
|
||||
PathBuf::from("tmp/rule.py"),
|
||||
PathBuf::from("pyproject.toml"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("tmp/rule.py"),
|
||||
PathBuf::from("pyproject.toml"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
None,
|
||||
change_detected(&[
|
||||
PathBuf::from("tmp/rule.js"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
]),
|
||||
change_detected(¬ify::Event {
|
||||
kind: notify::EventKind::Create(notify::event::CreateKind::File),
|
||||
paths: vec![
|
||||
PathBuf::from("tmp/rule.js"),
|
||||
PathBuf::from("tmp/bin/ruff.rs"),
|
||||
],
|
||||
attrs: notify::event::EventAttributes::default(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,125 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::{
|
||||
files::File,
|
||||
source::{line_index, source_text},
|
||||
Db,
|
||||
};
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_text_size::TextRange;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// A string identifier for a lint rule.
|
||||
///
|
||||
/// This string is used in command line and configuration interfaces. The name should always
|
||||
/// be in kebab case, e.g. `no-foo` (all lower case).
|
||||
///
|
||||
/// Rules use kebab case, e.g. `no-foo`.
|
||||
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct LintName(&'static str);
|
||||
|
||||
impl LintName {
|
||||
pub const fn of(name: &'static str) -> Self {
|
||||
Self(name)
|
||||
}
|
||||
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for LintName {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LintName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for LintName {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
self.0 == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for LintName {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.0 == *other
|
||||
}
|
||||
}
|
||||
|
||||
/// Uniquely identifies the kind of a diagnostic.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
pub enum DiagnosticId {
|
||||
/// Some I/O operation failed
|
||||
Io,
|
||||
|
||||
/// Some code contains a syntax error
|
||||
InvalidSyntax,
|
||||
|
||||
/// A lint violation.
|
||||
///
|
||||
/// Lint's can be suppressed and some lints can be enabled or disabled in the configuration.
|
||||
Lint(LintName),
|
||||
|
||||
/// Some code is incorrectly formatted.
|
||||
Format,
|
||||
|
||||
/// A revealed type: Created by `reveal_type(expression)`.
|
||||
RevealedType,
|
||||
}
|
||||
|
||||
impl DiagnosticId {
|
||||
/// Creates a new `DiagnosticId` for a lint with the given name.
|
||||
pub const fn lint(name: &'static str) -> Self {
|
||||
Self::Lint(LintName::of(name))
|
||||
}
|
||||
|
||||
/// Returns `true` if this `DiagnosticId` represents a lint.
|
||||
pub fn is_lint(&self) -> bool {
|
||||
matches!(self, DiagnosticId::Lint(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if this `DiagnosticId` represents a lint with the given name.
|
||||
pub fn is_lint_named(&self, name: &str) -> bool {
|
||||
matches!(self, DiagnosticId::Lint(self_name) if self_name == name)
|
||||
}
|
||||
|
||||
pub fn matches(&self, name: &str) -> bool {
|
||||
match self {
|
||||
DiagnosticId::Lint(self_name) => name
|
||||
.strip_prefix("lint/")
|
||||
.is_some_and(|rest| rest == &**self_name),
|
||||
|
||||
DiagnosticId::Io => name == "io",
|
||||
DiagnosticId::InvalidSyntax => name == "invalid-syntax",
|
||||
DiagnosticId::Format => name == "format",
|
||||
DiagnosticId::RevealedType => name == "revealed-type",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DiagnosticId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DiagnosticId::InvalidSyntax => f.write_str("invalid-syntax"),
|
||||
DiagnosticId::Io => f.write_str("io"),
|
||||
DiagnosticId::Lint(name) => write!(f, "lint/{name}"),
|
||||
DiagnosticId::Format => f.write_str("format"),
|
||||
DiagnosticId::RevealedType => f.write_str("revealed-type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Diagnostic: Send + Sync + std::fmt::Debug {
|
||||
fn rule(&self) -> &str;
|
||||
fn id(&self) -> DiagnosticId;
|
||||
|
||||
fn message(&self) -> std::borrow::Cow<str>;
|
||||
|
||||
@@ -29,10 +140,12 @@ pub trait Diagnostic: Send + Sync + std::fmt::Debug {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
pub struct DisplayDiagnostic<'db> {
|
||||
@@ -50,13 +163,15 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.diagnostic.severity() {
|
||||
Severity::Info => f.write_str("info")?,
|
||||
Severity::Warning => f.write_str("warning")?,
|
||||
Severity::Error => f.write_str("error")?,
|
||||
Severity::Fatal => f.write_str("fatal")?,
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"[{rule}] {path}",
|
||||
rule = self.diagnostic.rule(),
|
||||
rule = self.diagnostic.id(),
|
||||
path = self.diagnostic.file().path(self.db)
|
||||
)?;
|
||||
|
||||
@@ -77,8 +192,8 @@ impl<T> Diagnostic for Box<T>
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
fn id(&self) -> DiagnosticId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
@@ -102,8 +217,8 @@ impl<T> Diagnostic for std::sync::Arc<T>
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
fn id(&self) -> DiagnosticId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn message(&self) -> std::borrow::Cow<str> {
|
||||
@@ -124,8 +239,8 @@ where
|
||||
}
|
||||
|
||||
impl Diagnostic for Box<dyn Diagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
fn id(&self) -> DiagnosticId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
@@ -158,8 +273,8 @@ impl ParseDiagnostic {
|
||||
}
|
||||
|
||||
impl Diagnostic for ParseDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"invalid-syntax"
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::InvalidSyntax
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
|
||||
@@ -299,7 +299,8 @@ where
|
||||
///
|
||||
/// - Removes [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::Soft).
|
||||
/// - Replaces [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::SoftOrSpace) with a [`Space`](FormatElement::Space)
|
||||
/// - Removes [`if_group_breaks`](crate::builders::if_group_breaks) elements.
|
||||
/// - Removes [`if_group_breaks`](crate::builders::if_group_breaks) and all its content.
|
||||
/// - Unwraps the content of [`if_group_fits_on_line`](crate::builders::if_group_fits_on_line) elements (but retains it).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -387,7 +388,7 @@ fn clean_interned(
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(index, element)| match element {
|
||||
FormatElement::Line(LineMode::Soft | LineMode::SoftOrSpace) => {
|
||||
FormatElement::Line(LineMode::SoftOrSpace) => {
|
||||
let mut cleaned = Vec::new();
|
||||
let (before, after) = interned.split_at(index);
|
||||
cleaned.extend_from_slice(before);
|
||||
@@ -427,7 +428,6 @@ fn clean_interned(
|
||||
}
|
||||
|
||||
let element = match element {
|
||||
FormatElement::Line(LineMode::Soft) => continue,
|
||||
FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space,
|
||||
FormatElement::Interned(interned) => {
|
||||
FormatElement::Interned(clean_interned(interned, interned_cache))
|
||||
@@ -458,7 +458,6 @@ impl<Context> Buffer for RemoveSoftLinesBuffer<'_, Context> {
|
||||
}
|
||||
|
||||
let element = match element {
|
||||
FormatElement::Line(LineMode::Soft) => return,
|
||||
FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space,
|
||||
FormatElement::Interned(interned) => {
|
||||
FormatElement::Interned(self.clean_interned(&interned))
|
||||
@@ -508,28 +507,34 @@ enum RemoveSoftLineBreaksState {
|
||||
impl RemoveSoftLineBreaksState {
|
||||
fn should_drop(&mut self, element: &FormatElement) -> bool {
|
||||
match self {
|
||||
Self::Default => {
|
||||
// Entered the start of an `if_group_breaks`
|
||||
if let FormatElement::Tag(Tag::StartConditionalContent(condition)) = element {
|
||||
Self::Default => match element {
|
||||
FormatElement::Line(LineMode::Soft) => true,
|
||||
|
||||
// Entered the start of an `if_group_breaks` or `if_group_fits`
|
||||
// For `if_group_breaks`: Remove the start and end tag and all content in between.
|
||||
// For `if_group_fits_on_line`: Unwrap the content. This is important because the enclosing group
|
||||
// might still *expand* if the content exceeds the line width limit, in which case the
|
||||
// `if_group_fits_on_line` content would be removed.
|
||||
FormatElement::Tag(Tag::StartConditionalContent(condition)) => {
|
||||
if condition.mode.is_expanded() {
|
||||
*self = Self::InIfGroupBreaks {
|
||||
conditional_content_level: NonZeroUsize::new(1).unwrap(),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
FormatElement::Tag(Tag::EndConditionalContent) => true,
|
||||
_ => false,
|
||||
},
|
||||
Self::InIfGroupBreaks {
|
||||
conditional_content_level,
|
||||
} => {
|
||||
match element {
|
||||
// A nested `if_group_breaks` or `if_group_fits`
|
||||
// A nested `if_group_breaks` or `if_group_fits_on_line`
|
||||
FormatElement::Tag(Tag::StartConditionalContent(_)) => {
|
||||
*conditional_content_level = conditional_content_level.saturating_add(1);
|
||||
}
|
||||
// The end of an `if_group_breaks` or `if_group_fits`.
|
||||
// The end of an `if_group_breaks` or `if_group_fits_on_line`.
|
||||
FormatElement::Tag(Tag::EndConditionalContent) => {
|
||||
if let Some(level) = NonZeroUsize::new(conditional_content_level.get() - 1)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use zip::CompressionMethod;
|
||||
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
use red_knot_python_semantic::{Db, Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{OsSystem, System, SystemPathBuf};
|
||||
@@ -19,6 +21,7 @@ pub struct ModuleDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: OsSystem,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
impl ModuleDb {
|
||||
@@ -60,6 +63,7 @@ impl ModuleDb {
|
||||
storage: self.storage.clone(),
|
||||
system: self.system.clone(),
|
||||
files: self.files.snapshot(),
|
||||
rule_selection: Arc::clone(&self.rule_selection),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +97,10 @@ impl Db for ModuleDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
from airflow import DAG, dag
|
||||
from airflow.timetables.simple import NullTimetable
|
||||
|
||||
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
|
||||
from airflow.providers.standard.operators import trigger_dagrun
|
||||
|
||||
from airflow.operators.datetime import BranchDateTimeOperator
|
||||
from airflow.providers.standard.operators import datetime
|
||||
|
||||
from airflow.sensors.weekday import DayOfWeekSensor, BranchDayOfWeekOperator
|
||||
from airflow.providers.standard.sensors import weekday
|
||||
|
||||
DAG(dag_id="class_schedule", schedule="@hourly")
|
||||
|
||||
DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
|
||||
@@ -8,6 +17,13 @@ DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
|
||||
DAG(dag_id="class_timetable", timetable=NullTimetable())
|
||||
|
||||
|
||||
def sla_callback(*arg, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback)
|
||||
|
||||
|
||||
@dag(schedule="0 * * * *")
|
||||
def decorator_schedule():
|
||||
pass
|
||||
@@ -21,3 +37,42 @@ def decorator_schedule_interval():
|
||||
@dag(timetable=NullTimetable())
|
||||
def decorator_timetable():
|
||||
pass
|
||||
|
||||
|
||||
@dag(sla_miss_callback=sla_callback)
|
||||
def decorator_sla_callback():
|
||||
pass
|
||||
|
||||
|
||||
@dag()
|
||||
def decorator_deprecated_operator_args():
|
||||
trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
|
||||
task_id="trigger_dagrun_op1", execution_date="2024-12-04"
|
||||
)
|
||||
trigger_dagrun_op2 = TriggerDagRunOperator(
|
||||
task_id="trigger_dagrun_op2", execution_date="2024-12-04"
|
||||
)
|
||||
|
||||
branch_dt_op = datetime.BranchDateTimeOperator(
|
||||
task_id="branch_dt_op", use_task_execution_day=True
|
||||
)
|
||||
branch_dt_op2 = BranchDateTimeOperator(
|
||||
task_id="branch_dt_op2", use_task_execution_day=True
|
||||
)
|
||||
|
||||
dof_task_sensor = weekday.DayOfWeekSensor(
|
||||
task_id="dof_task_sensor", use_task_execution_day=True
|
||||
)
|
||||
dof_task_sensor2 = DayOfWeekSensor(
|
||||
task_id="dof_task_sensor2", use_task_execution_day=True
|
||||
)
|
||||
|
||||
bdow_op = weekday.BranchDayOfWeekOperator(
|
||||
task_id="bdow_op", use_task_execution_day=True
|
||||
)
|
||||
bdow_op2 = BranchDayOfWeekOperator(task_id="bdow_op2", use_task_execution_day=True)
|
||||
|
||||
trigger_dagrun_op >> trigger_dagrun_op2
|
||||
branch_dt_op >> branch_dt_op2
|
||||
dof_task_sensor >> dof_task_sensor2
|
||||
bdow_op >> bdow_op2
|
||||
|
||||
@@ -1,8 +1,35 @@
|
||||
from airflow import PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
from airflow.triggers.external_task import TaskStateTrigger
|
||||
from airflow.www.auth import has_access
|
||||
from airflow.api_connexion.security import requires_access
|
||||
from airflow.configuration import (
|
||||
get,
|
||||
getboolean,
|
||||
getfloat,
|
||||
getint,
|
||||
has_option,
|
||||
remove_option,
|
||||
as_dict,
|
||||
set,
|
||||
)
|
||||
from airflow.contrib.aws_athena_hook import AWSAthenaHook
|
||||
from airflow.metrics.validators import AllowListValidator
|
||||
from airflow.metrics.validators import BlockListValidator
|
||||
from airflow.operators.subdag import SubDagOperator
|
||||
from airflow.sensors.external_task import ExternalTaskSensorLink
|
||||
from airflow.operators.bash_operator import BashOperator
|
||||
from airflow.operators.branch_operator import BaseBranchOperator
|
||||
from airflow.operators.dummy import EmptyOperator, DummyOperator
|
||||
from airflow.operators import dummy_operator
|
||||
from airflow.operators.email_operator import EmailOperator
|
||||
from airflow.sensors.base_sensor_operator import BaseSensorOperator
|
||||
from airflow.sensors.date_time_sensor import DateTimeSensor
|
||||
from airflow.sensors.external_task_sensor import (
|
||||
ExternalTaskMarker,
|
||||
ExternalTaskSensor,
|
||||
ExternalTaskSensorLink,
|
||||
)
|
||||
from airflow.sensors.time_delta_sensor import TimeDeltaSensor
|
||||
from airflow.secrets.local_filesystem import get_connection, load_connections
|
||||
from airflow.utils import dates
|
||||
from airflow.utils.dates import (
|
||||
date_range,
|
||||
@@ -13,20 +40,27 @@ from airflow.utils.dates import (
|
||||
round_time,
|
||||
scale_time_units,
|
||||
)
|
||||
from airflow.utils.decorators import apply_defaults
|
||||
from airflow.utils.file import TemporaryDirectory, mkdirs
|
||||
from airflow.utils.helpers import chain, cross_downstream
|
||||
from airflow.utils.state import SHUTDOWN, terminating_states
|
||||
from airflow.utils.dag_cycle_tester import test_cycle
|
||||
from airflow.utils.trigger_rule import TriggerRule
|
||||
from airflow.www.auth import has_access
|
||||
from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
|
||||
|
||||
PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
|
||||
AWSAthenaHook
|
||||
TaskStateTrigger
|
||||
|
||||
|
||||
has_access
|
||||
requires_access
|
||||
|
||||
AllowListValidator
|
||||
BlockListValidator
|
||||
|
||||
SubDagOperator
|
||||
|
||||
dates.date_range
|
||||
dates.days_ago
|
||||
|
||||
@@ -42,11 +76,38 @@ infer_time_unit
|
||||
datetime_to_nano
|
||||
dates.datetime_to_nano
|
||||
|
||||
get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
|
||||
get_connection, load_connections
|
||||
|
||||
|
||||
ExternalTaskSensorLink
|
||||
BashOperator
|
||||
BaseBranchOperator
|
||||
EmptyOperator, DummyOperator
|
||||
dummy_operator.EmptyOperator
|
||||
dummy_operator.DummyOperator
|
||||
EmailOperator
|
||||
BaseSensorOperator
|
||||
DateTimeSensor
|
||||
(ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
|
||||
TimeDeltaSensor
|
||||
|
||||
apply_defaults
|
||||
|
||||
TemporaryDirectory
|
||||
mkdirs
|
||||
|
||||
chain
|
||||
cross_downstream
|
||||
|
||||
SHUTDOWN
|
||||
terminating_states
|
||||
|
||||
TriggerRule.DUMMY
|
||||
TriggerRule.NONE_FAILED_OR_SKIPPED
|
||||
|
||||
test_cycle
|
||||
|
||||
has_access
|
||||
get_sensitive_variables_fields, should_hide_value_for_key
|
||||
|
||||
@@ -151,6 +151,6 @@ PRESELECT *
|
||||
FROM {var}.table
|
||||
"""
|
||||
|
||||
# to be handled seperately
|
||||
# to be handled separately
|
||||
# query58 = f"SELECT\
|
||||
# * FROM {var}.table"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Should emit:
|
||||
B025 - on lines 15, 22, 31
|
||||
B025 - on lines 15, 22, 31, 40, 47, 56
|
||||
"""
|
||||
|
||||
import pickle
|
||||
@@ -36,3 +36,28 @@ except ValueError:
|
||||
a = 2
|
||||
except (OSError, TypeError):
|
||||
a = 2
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except* ValueError:
|
||||
a = 2
|
||||
except* ValueError:
|
||||
a = 2
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except* pickle.PickleError:
|
||||
a = 2
|
||||
except* ValueError:
|
||||
a = 2
|
||||
except* pickle.PickleError:
|
||||
a = 2
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except* (ValueError, TypeError):
|
||||
a = 2
|
||||
except* ValueError:
|
||||
a = 2
|
||||
except* (OSError, TypeError):
|
||||
a = 2
|
||||
|
||||
@@ -9,3 +9,9 @@ warnings.warn(DeprecationWarning("test"))
|
||||
warnings.warn(DeprecationWarning("test"), source=None)
|
||||
warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2)
|
||||
warnings.warn(DeprecationWarning("test"), stacklevel=1)
|
||||
|
||||
warnings.warn(
|
||||
DeprecationWarning("test"),
|
||||
# some comments here
|
||||
source = None # no trailing comma
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Should emit:
|
||||
B029 - on lines 8 and 13
|
||||
B029 - on lines 8, 13, 18 and 23
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -11,4 +11,14 @@ except ():
|
||||
try:
|
||||
pass
|
||||
except () as e:
|
||||
pass
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ():
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* () as e:
|
||||
pass
|
||||
|
||||
@@ -130,3 +130,23 @@ try:
|
||||
pass
|
||||
except (a, b) * (c, d): # B030
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* 1: # Error
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (1, ValueError): # Error
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError, (RuntimeError, (KeyError, TypeError))): # Error
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (a, b) * (c, d): # B030
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Should emit:
|
||||
B904 - on lines 10, 11, 16, 62, and 64
|
||||
B904 - on lines 10, 11, 16, 62, 64, 79, 81, 87, 88, 93 and 97
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -71,3 +71,29 @@ except Exception as e:
|
||||
match 0:
|
||||
case 0:
|
||||
raise RuntimeError("boom!")
|
||||
|
||||
try:
|
||||
...
|
||||
except* Exception as e:
|
||||
if ...:
|
||||
raise RuntimeError("boom!")
|
||||
else:
|
||||
raise RuntimeError("bang!")
|
||||
|
||||
try:
|
||||
raise ValueError
|
||||
except* ValueError:
|
||||
if "abc":
|
||||
raise TypeError
|
||||
raise UserWarning
|
||||
except* AssertionError:
|
||||
raise # Bare `raise` should not be an error
|
||||
except* Exception as err:
|
||||
assert err
|
||||
raise Exception("No cause here...")
|
||||
except* BaseException as err:
|
||||
raise err
|
||||
except* BaseException as err:
|
||||
raise some_other_err
|
||||
finally:
|
||||
raise Exception("Nothing to chain from, so no warning here")
|
||||
|
||||
@@ -47,3 +47,13 @@ dict(map(lambda k, v: (k, v), keys, values))
|
||||
map(lambda x: x, y if y else z)
|
||||
map(lambda x: x, (y if y else z))
|
||||
map(lambda x: x, (x, y, z))
|
||||
|
||||
# See https://github.com/astral-sh/ruff/issues/14808
|
||||
# The following should be Ok since
|
||||
# named expressions are a syntax error inside comprehensions
|
||||
a = [1, 2, 3]
|
||||
b = map(lambda x: x, c := a)
|
||||
print(c)
|
||||
|
||||
# Check nested as well
|
||||
map(lambda x:x, [c:=a])
|
||||
|
||||
@@ -67,5 +67,16 @@ def not_a_deprecated_function() -> None: ...
|
||||
|
||||
fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053
|
||||
|
||||
from typing import TypeAlias, Literal, Annotated
|
||||
|
||||
# see https://github.com/astral-sh/ruff/issues/12995
|
||||
def foo(bar: typing.Literal["a", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]):...
|
||||
def foo(bar: typing.Literal["a", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]):...
|
||||
|
||||
# Ok
|
||||
def f(x: int) -> "AnnotationsForClassesWithVeryLongNamesInQuotesAsReturnTypes":...
|
||||
|
||||
# Ok
|
||||
x: TypeAlias = Literal["fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooO"]
|
||||
|
||||
# Ok
|
||||
y: TypeAlias = Annotated[int, "metadataaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
|
||||
|
||||
@@ -81,3 +81,78 @@ def test_not_decorator(param1, param2):
|
||||
@pytest.mark.parametrize(argnames=("param1,param2"), argvalues=[(1, 2), (3, 4)])
|
||||
def test_keyword_arguments(param1, param2):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("param",), [(1,), (2,)])
|
||||
def test_single_element_tuple(param):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("param",), [[1], [2]])
|
||||
def test_single_element_list(param):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("param",), [[1], [2]])
|
||||
def test_single_element_list(param):
|
||||
...
|
||||
|
||||
|
||||
# Unsafe fix
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
# comment
|
||||
"param",
|
||||
),
|
||||
[[1], [2]],
|
||||
)
|
||||
def test_comment_in_argnames(param):
|
||||
...
|
||||
|
||||
# Unsafe fix
|
||||
@pytest.mark.parametrize(
|
||||
("param",),
|
||||
[
|
||||
(
|
||||
# comment
|
||||
1,
|
||||
),
|
||||
(2,),
|
||||
],
|
||||
)
|
||||
def test_comment_in_argvalues(param):
|
||||
...
|
||||
|
||||
|
||||
# Safe fix
|
||||
@pytest.mark.parametrize(
|
||||
("param",),
|
||||
[
|
||||
(1,),
|
||||
# comment
|
||||
(2,),
|
||||
],
|
||||
)
|
||||
def test_comment_between_argvalues_items(param):
|
||||
...
|
||||
|
||||
|
||||
# A fix should be suggested for `argnames`, but not for `argvalues`.
|
||||
@pytest.mark.parametrize(
|
||||
("param",),
|
||||
[
|
||||
(1,),
|
||||
(2, 3),
|
||||
],
|
||||
)
|
||||
def test_invalid_argvalues(param):
|
||||
"""
|
||||
pytest throws the following error for this test:
|
||||
------------------------------------------------
|
||||
a.py::test_comment_between_argvalues_items: in "parametrize" the number of names (1):
|
||||
('param',)
|
||||
must be equal to the number of values (2):
|
||||
(2, 3)
|
||||
------------------------------------------------
|
||||
"""
|
||||
...
|
||||
|
||||
5
crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT006_and_PT007.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT006_and_PT007.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import pytest
|
||||
|
||||
@pytest.mark.parametrize(("param",), [[1], [2]])
|
||||
def test_PT006_and_PT007_do_not_conflict(param):
|
||||
...
|
||||
99
crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH210.py
vendored
Normal file
99
crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH210.py
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
from pathlib import (
|
||||
Path,
|
||||
PosixPath,
|
||||
PurePath,
|
||||
PurePosixPath,
|
||||
PureWindowsPath,
|
||||
WindowsPath,
|
||||
)
|
||||
import pathlib
|
||||
|
||||
|
||||
path = Path()
|
||||
posix_path: pathlib.PosixPath = PosixPath()
|
||||
pure_path: PurePath = PurePath()
|
||||
pure_posix_path = pathlib.PurePosixPath()
|
||||
pure_windows_path: PureWindowsPath = pathlib.PureWindowsPath()
|
||||
windows_path: pathlib.WindowsPath = pathlib.WindowsPath()
|
||||
|
||||
|
||||
### Errors
|
||||
path.with_suffix("py")
|
||||
path.with_suffix(r"s")
|
||||
path.with_suffix(u'' "json")
|
||||
path.with_suffix(suffix="js")
|
||||
|
||||
posix_path.with_suffix("py")
|
||||
posix_path.with_suffix(r"s")
|
||||
posix_path.with_suffix(u'' "json")
|
||||
posix_path.with_suffix(suffix="js")
|
||||
|
||||
pure_path.with_suffix("py")
|
||||
pure_path.with_suffix(r"s")
|
||||
pure_path.with_suffix(u'' "json")
|
||||
pure_path.with_suffix(suffix="js")
|
||||
|
||||
pure_posix_path.with_suffix("py")
|
||||
pure_posix_path.with_suffix(r"s")
|
||||
pure_posix_path.with_suffix(u'' "json")
|
||||
pure_posix_path.with_suffix(suffix="js")
|
||||
|
||||
pure_windows_path.with_suffix("py")
|
||||
pure_windows_path.with_suffix(r"s")
|
||||
pure_windows_path.with_suffix(u'' "json")
|
||||
pure_windows_path.with_suffix(suffix="js")
|
||||
|
||||
windows_path.with_suffix("py")
|
||||
windows_path.with_suffix(r"s")
|
||||
windows_path.with_suffix(u'' "json")
|
||||
windows_path.with_suffix(suffix="js")
|
||||
|
||||
|
||||
### No errors
|
||||
path.with_suffix()
|
||||
path.with_suffix('')
|
||||
path.with_suffix(".py")
|
||||
path.with_suffix("foo", "bar")
|
||||
path.with_suffix(suffix)
|
||||
path.with_suffix(f"oo")
|
||||
path.with_suffix(b"ar")
|
||||
|
||||
posix_path.with_suffix()
|
||||
posix_path.with_suffix('')
|
||||
posix_path.with_suffix(".py")
|
||||
posix_path.with_suffix("foo", "bar")
|
||||
posix_path.with_suffix(suffix)
|
||||
posix_path.with_suffix(f"oo")
|
||||
posix_path.with_suffix(b"ar")
|
||||
|
||||
pure_path.with_suffix()
|
||||
pure_path.with_suffix('')
|
||||
pure_path.with_suffix(".py")
|
||||
pure_path.with_suffix("foo", "bar")
|
||||
pure_path.with_suffix(suffix)
|
||||
pure_path.with_suffix(f"oo")
|
||||
pure_path.with_suffix(b"ar")
|
||||
|
||||
pure_posix_path.with_suffix()
|
||||
pure_posix_path.with_suffix('')
|
||||
pure_posix_path.with_suffix(".py")
|
||||
pure_posix_path.with_suffix("foo", "bar")
|
||||
pure_posix_path.with_suffix(suffix)
|
||||
pure_posix_path.with_suffix(f"oo")
|
||||
pure_posix_path.with_suffix(b"ar")
|
||||
|
||||
pure_windows_path.with_suffix()
|
||||
pure_windows_path.with_suffix('')
|
||||
pure_windows_path.with_suffix(".py")
|
||||
pure_windows_path.with_suffix("foo", "bar")
|
||||
pure_windows_path.with_suffix(suffix)
|
||||
pure_windows_path.with_suffix(f"oo")
|
||||
pure_windows_path.with_suffix(b"ar")
|
||||
|
||||
windows_path.with_suffix()
|
||||
windows_path.with_suffix('')
|
||||
windows_path.with_suffix(".py")
|
||||
windows_path.with_suffix("foo", "bar")
|
||||
windows_path.with_suffix(suffix)
|
||||
windows_path.with_suffix(f"oo")
|
||||
windows_path.with_suffix(b"ar")
|
||||
110
crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH210_1.py
vendored
Normal file
110
crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH210_1.py
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
from pathlib import (
|
||||
Path,
|
||||
PosixPath,
|
||||
PurePath,
|
||||
PurePosixPath,
|
||||
PureWindowsPath,
|
||||
WindowsPath,
|
||||
)
|
||||
|
||||
|
||||
def test_path(p: Path) -> None:
|
||||
## Errors
|
||||
p.with_suffix("py")
|
||||
p.with_suffix(r"s")
|
||||
p.with_suffix(u'' "json")
|
||||
p.with_suffix(suffix="js")
|
||||
|
||||
## No errors
|
||||
p.with_suffix()
|
||||
p.with_suffix('')
|
||||
p.with_suffix(".py")
|
||||
p.with_suffix("foo", "bar")
|
||||
p.with_suffix(suffix)
|
||||
p.with_suffix(f"oo")
|
||||
p.with_suffix(b"ar")
|
||||
|
||||
|
||||
def test_posix_path(p: PosixPath) -> None:
|
||||
## Errors
|
||||
p.with_suffix("py")
|
||||
p.with_suffix(r"s")
|
||||
p.with_suffix(u'' "json")
|
||||
p.with_suffix(suffix="js")
|
||||
|
||||
## No errors
|
||||
p.with_suffix()
|
||||
p.with_suffix('')
|
||||
p.with_suffix(".py")
|
||||
p.with_suffix("foo", "bar")
|
||||
p.with_suffix(suffix)
|
||||
p.with_suffix(f"oo")
|
||||
p.with_suffix(b"ar")
|
||||
|
||||
|
||||
def test_pure_path(p: PurePath) -> None:
|
||||
## Errors
|
||||
p.with_suffix("py")
|
||||
p.with_suffix(r"s")
|
||||
p.with_suffix(u'' "json")
|
||||
p.with_suffix(suffix="js")
|
||||
|
||||
## No errors
|
||||
p.with_suffix()
|
||||
p.with_suffix('')
|
||||
p.with_suffix(".py")
|
||||
p.with_suffix("foo", "bar")
|
||||
p.with_suffix(suffix)
|
||||
p.with_suffix(f"oo")
|
||||
p.with_suffix(b"ar")
|
||||
|
||||
|
||||
def test_pure_posix_path(p: PurePosixPath) -> None:
|
||||
## Errors
|
||||
p.with_suffix("py")
|
||||
p.with_suffix(r"s")
|
||||
p.with_suffix(u'' "json")
|
||||
p.with_suffix(suffix="js")
|
||||
|
||||
## No errors
|
||||
p.with_suffix()
|
||||
p.with_suffix('')
|
||||
p.with_suffix(".py")
|
||||
p.with_suffix("foo", "bar")
|
||||
p.with_suffix(suffix)
|
||||
p.with_suffix(f"oo")
|
||||
p.with_suffix(b"ar")
|
||||
|
||||
|
||||
def test_pure_windows_path(p: PureWindowsPath) -> None:
|
||||
## Errors
|
||||
p.with_suffix("py")
|
||||
p.with_suffix(r"s")
|
||||
p.with_suffix(u'' "json")
|
||||
p.with_suffix(suffix="js")
|
||||
|
||||
## No errors
|
||||
p.with_suffix()
|
||||
p.with_suffix('')
|
||||
p.with_suffix(".py")
|
||||
p.with_suffix("foo", "bar")
|
||||
p.with_suffix(suffix)
|
||||
p.with_suffix(f"oo")
|
||||
p.with_suffix(b"ar")
|
||||
|
||||
|
||||
def test_windows_path(p: WindowsPath) -> None:
|
||||
## Errors
|
||||
p.with_suffix("py")
|
||||
p.with_suffix(r"s")
|
||||
p.with_suffix(u'' "json")
|
||||
p.with_suffix(suffix="js")
|
||||
|
||||
## No errors
|
||||
p.with_suffix()
|
||||
p.with_suffix('')
|
||||
p.with_suffix(".py")
|
||||
p.with_suffix("foo", "bar")
|
||||
p.with_suffix(suffix)
|
||||
p.with_suffix(f"oo")
|
||||
p.with_suffix(b"ar")
|
||||
@@ -178,7 +178,7 @@ def foo(x: int) -> int | None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
x (int): An interger.
|
||||
x (int): An integer.
|
||||
"""
|
||||
if x < 0:
|
||||
return None
|
||||
@@ -191,7 +191,7 @@ def foo(x):
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
x (int): An interger.
|
||||
x (int): An integer.
|
||||
"""
|
||||
if x < 0:
|
||||
return None
|
||||
|
||||
@@ -154,7 +154,7 @@ def foo(x: int) -> int | None:
|
||||
Parameters
|
||||
----------
|
||||
x : int
|
||||
An interger.
|
||||
An integer.
|
||||
"""
|
||||
if x < 0:
|
||||
return None
|
||||
@@ -169,7 +169,7 @@ def foo(x):
|
||||
Parameters
|
||||
----------
|
||||
x : int
|
||||
An interger.
|
||||
An integer.
|
||||
"""
|
||||
if x < 0:
|
||||
return None
|
||||
|
||||
@@ -119,10 +119,30 @@ c = int(input())
|
||||
if a > b and b < c:
|
||||
pass
|
||||
|
||||
|
||||
# Unfixable due to parentheses.
|
||||
# fixes will balance parentheses
|
||||
(a < b) and b < c
|
||||
a < b and (b < c)
|
||||
((a < b) and b < c)
|
||||
(a < b) and (b < c)
|
||||
(((a < b))) and (b < c)
|
||||
|
||||
(a<b) and b<c and ((c<d))
|
||||
|
||||
# should error and fix
|
||||
a<b<c and c<d
|
||||
|
||||
# more involved examples (all should error and fix)
|
||||
a < ( # sneaky comment
|
||||
b
|
||||
# more comments
|
||||
) and b < c
|
||||
|
||||
(
|
||||
a
|
||||
<b
|
||||
# hmmm...
|
||||
<c
|
||||
and ((c<d))
|
||||
)
|
||||
|
||||
a < (b) and (((b)) < c)
|
||||
|
||||
@@ -13,3 +13,5 @@ os.getenv("B", Z)
|
||||
os.getenv("AA", "GOOD" if Z else "BAR")
|
||||
os.getenv("AA", 1 if Z else "BAR") # [invalid-envvar-default]
|
||||
os.environ.get("TEST", 12) # [invalid-envvar-default]
|
||||
os.environ.get("TEST", "AA" * 12)
|
||||
os.environ.get("TEST", 13 * "AA")
|
||||
|
||||
@@ -41,3 +41,8 @@ MyType = TypedDict("MyType", {})
|
||||
|
||||
# Empty dict call
|
||||
MyType = TypedDict("MyType", dict())
|
||||
|
||||
# Unsafe fix if comments are present
|
||||
X = TypedDict("X", {
|
||||
"some_config": int, # important
|
||||
})
|
||||
|
||||
@@ -31,3 +31,9 @@ S3File = NamedTuple(
|
||||
("dataHPK",* str),
|
||||
],
|
||||
)
|
||||
|
||||
# Unsafe fix if comments are present
|
||||
X = NamedTuple("X", [
|
||||
("some_config", int), # important
|
||||
])
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ t.Literal[1, t.Literal[2, t.Literal[1]]]
|
||||
Literal[
|
||||
1, # comment 1
|
||||
Literal[ # another comment
|
||||
1 # yet annother comment
|
||||
1 # yet another comment
|
||||
]
|
||||
] # once
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ t.Literal[1, t.Literal[2, t.Literal[1]]]
|
||||
Literal[
|
||||
1, # comment 1
|
||||
Literal[ # another comment
|
||||
1 # yet annother comment
|
||||
1 # yet another comment
|
||||
]
|
||||
] # once
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class Class_:
|
||||
_var = "method variable" # [RUF052]
|
||||
return _var
|
||||
|
||||
def fun(_var): # [RUF052]
|
||||
def fun(_var): # parameters are ignored
|
||||
return _var
|
||||
|
||||
def fun():
|
||||
@@ -129,3 +129,19 @@ def unfixables():
|
||||
# unfixable because the rename would shadow a variable from the outer function
|
||||
_local = "local4"
|
||||
print(_local)
|
||||
|
||||
def special_calls():
|
||||
from typing import TypeVar, ParamSpec, NamedTuple
|
||||
from enum import Enum
|
||||
from collections import namedtuple
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
_NT = NamedTuple("_NT", [("foo", int)])
|
||||
_E = Enum("_E", ["a", "b", "c"])
|
||||
_NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
|
||||
_NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
|
||||
_DynamicClass = type("_DynamicClass", (), {})
|
||||
_NotADynamicClass = type("_NotADynamicClass")
|
||||
|
||||
print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
|
||||
|
||||
@@ -1096,6 +1096,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryCastToInt) {
|
||||
ruff::rules::unnecessary_cast_to_int(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::DotlessPathlibWithSuffix) {
|
||||
flake8_use_pathlib::rules::dotless_pathlib_with_suffix(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(dict) => {
|
||||
if checker.any_enabled(&[
|
||||
|
||||
@@ -909,6 +909,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "206") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsSepSplit),
|
||||
(Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob),
|
||||
(Flake8UsePathlib, "208") => (RuleGroup::Preview, rules::flake8_use_pathlib::violations::OsListdir),
|
||||
(Flake8UsePathlib, "210") => (RuleGroup::Preview, rules::flake8_use_pathlib::rules::DotlessPathlibWithSuffix),
|
||||
|
||||
// flake8-logging-format
|
||||
(Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat),
|
||||
|
||||
@@ -4,6 +4,7 @@ use anyhow::{anyhow, Result};
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -69,6 +70,11 @@ impl Renamer {
|
||||
/// example, to rename `pandas` to `pd`, we may need to rewrite `import pandas` to
|
||||
/// `import pandas as pd`, rather than `import pd`.
|
||||
///
|
||||
/// 1. Check to see if the binding is assigned to a known special call where the first argument
|
||||
/// must be a string that is the same as the binding's name. For example,
|
||||
/// `T = TypeVar("_T")` will be rejected by a type checker; only `T = TypeVar("T")` will do.
|
||||
/// If it *is* one of these calls, we rename the relevant argument as well.
|
||||
///
|
||||
/// 1. Rename every reference to the [`Binding`]. For example, renaming the references to the
|
||||
/// `x = 1` binding above would give us:
|
||||
///
|
||||
@@ -198,6 +204,12 @@ impl Renamer {
|
||||
if let Some(edit) = Renamer::rename_binding(binding, name, target) {
|
||||
edits.push(edit);
|
||||
|
||||
if let Some(edit) =
|
||||
Renamer::fixup_assigned_value(binding, semantic, stylist, name, target)
|
||||
{
|
||||
edits.push(edit);
|
||||
}
|
||||
|
||||
// Rename any delayed annotations.
|
||||
if let Some(annotations) = semantic.delayed_annotations(binding_id) {
|
||||
edits.extend(annotations.iter().filter_map(|annotation_id| {
|
||||
@@ -231,6 +243,86 @@ impl Renamer {
|
||||
edits
|
||||
}
|
||||
|
||||
/// If the r.h.s. of a call expression is a call expression,
|
||||
/// we may need to fixup some arguments passed to that call expression.
|
||||
///
|
||||
/// It's impossible to do this entirely rigorously;
|
||||
/// we only special-case some common standard-library constructors here.
|
||||
///
|
||||
/// For example, in this `TypeVar` definition:
|
||||
/// ```py
|
||||
/// from typing import TypeVar
|
||||
///
|
||||
/// _T = TypeVar("_T")
|
||||
/// ```
|
||||
///
|
||||
/// If we're renaming it from `_T` to `T`, we want this to be the end result:
|
||||
/// ```py
|
||||
/// from typing import TypeVar
|
||||
///
|
||||
/// T = TypeVar("T")
|
||||
/// ```
|
||||
///
|
||||
/// Not this, which a type checker will reject:
|
||||
/// ```py
|
||||
/// from typing import TypeVar
|
||||
///
|
||||
/// T = TypeVar("_T")
|
||||
/// ```
|
||||
fn fixup_assigned_value(
|
||||
binding: &Binding,
|
||||
semantic: &SemanticModel,
|
||||
stylist: &Stylist,
|
||||
name: &str,
|
||||
target: &str,
|
||||
) -> Option<Edit> {
|
||||
let statement = binding.statement(semantic)?;
|
||||
|
||||
let (ast::Stmt::Assign(ast::StmtAssign { value, .. })
|
||||
| ast::Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
value: Some(value), ..
|
||||
})) = statement
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let ast::ExprCall {
|
||||
func, arguments, ..
|
||||
} = value.as_call_expr()?;
|
||||
|
||||
let qualified_name = semantic.resolve_qualified_name(func)?;
|
||||
|
||||
let name_argument = match qualified_name.segments() {
|
||||
["collections", "namedtuple"] => arguments.find_argument("typename", 0),
|
||||
|
||||
["typing" | "typing_extensions", "TypeVar" | "ParamSpec" | "TypeVarTuple" | "NewType" | "TypeAliasType"] => {
|
||||
arguments.find_argument("name", 0)
|
||||
}
|
||||
|
||||
["enum", "Enum" | "IntEnum" | "StrEnum" | "ReprEnum" | "Flag" | "IntFlag"]
|
||||
| ["typing" | "typing_extensions", "NamedTuple" | "TypedDict"] => {
|
||||
arguments.find_positional(0)
|
||||
}
|
||||
|
||||
["builtins" | "", "type"] if arguments.len() == 3 => arguments.find_positional(0),
|
||||
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let name_argument = name_argument.as_string_literal_expr()?;
|
||||
|
||||
if name_argument.value.to_str() != name {
|
||||
return None;
|
||||
}
|
||||
|
||||
let quote = stylist.quote();
|
||||
|
||||
Some(Edit::range_replacement(
|
||||
format!("{quote}{target}{quote}"),
|
||||
name_argument.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Rename a [`Binding`] reference.
|
||||
fn rename_binding(binding: &Binding, name: &str, target: &str) -> Option<Edit> {
|
||||
match &binding.kind {
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::checkers::ast::Checker;
|
||||
enum Replacement {
|
||||
None,
|
||||
Name(String),
|
||||
Message(String),
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
@@ -53,6 +54,9 @@ impl Violation for Airflow3Removal {
|
||||
Replacement::Name(name) => {
|
||||
format!("`{deprecated}` is removed in Airflow 3.0; use `{name}` instead")
|
||||
}
|
||||
Replacement::Message(message) => {
|
||||
format!("`{deprecated}` is removed in Airflow 3.0; {message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +96,39 @@ fn removed_argument(checker: &mut Checker, qualname: &QualifiedName, arguments:
|
||||
"timetable",
|
||||
Some("schedule"),
|
||||
));
|
||||
checker.diagnostics.extend(diagnostic_for_argument(
|
||||
arguments,
|
||||
"sla_miss_callback",
|
||||
None::<&str>,
|
||||
));
|
||||
}
|
||||
["airflow", .., "operators", "trigger_dagrun", "TriggerDagRunOperator"] => {
|
||||
checker.diagnostics.extend(diagnostic_for_argument(
|
||||
arguments,
|
||||
"execution_date",
|
||||
Some("logical_date"),
|
||||
));
|
||||
}
|
||||
["airflow", .., "operators", "datetime", "BranchDateTimeOperator"] => {
|
||||
checker.diagnostics.extend(diagnostic_for_argument(
|
||||
arguments,
|
||||
"use_task_execution_day",
|
||||
Some("use_task_logical_date"),
|
||||
));
|
||||
}
|
||||
["airflow", .., "operators", "weekday", "DayOfWeekSensor"] => {
|
||||
checker.diagnostics.extend(diagnostic_for_argument(
|
||||
arguments,
|
||||
"use_task_execution_day",
|
||||
Some("use_task_logical_date"),
|
||||
));
|
||||
}
|
||||
["airflow", .., "operators", "weekday", "BranchDayOfWeekOperator"] => {
|
||||
checker.diagnostics.extend(diagnostic_for_argument(
|
||||
arguments,
|
||||
"use_task_execution_day",
|
||||
Some("use_task_logical_date"),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@@ -106,16 +143,76 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
|
||||
["airflow", "triggers", "external_task", "TaskStateTrigger"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "www", "auth", "has_access"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.www.auth.has_access_*".to_string()),
|
||||
)),
|
||||
["airflow", "api_connexion", "security", "requires_access"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.api_connexion.security.requires_access_*".to_string(),
|
||||
),
|
||||
)),
|
||||
// airflow.PY\d{1,2}
|
||||
["airflow", "PY36"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("sys.version_info".to_string()),
|
||||
)),
|
||||
["airflow", "PY37"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("sys.version_info".to_string()),
|
||||
)),
|
||||
["airflow", "PY38"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("sys.version_info".to_string()),
|
||||
)),
|
||||
["airflow", "PY39"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("sys.version_info".to_string()),
|
||||
)),
|
||||
["airflow", "PY310"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("sys.version_info".to_string()),
|
||||
)),
|
||||
["airflow", "PY311"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("sys.version_info".to_string()),
|
||||
)),
|
||||
["airflow", "PY312"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("sys.version_info".to_string()),
|
||||
)),
|
||||
// airflow.configuration
|
||||
["airflow", "configuration", "get"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.configuration.conf.get".to_string()),
|
||||
)),
|
||||
["airflow", "configuration", "getboolean"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.configuration.conf.getboolean".to_string()),
|
||||
)),
|
||||
["airflow", "configuration", "getfloat"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.configuration.conf.getfloat".to_string()),
|
||||
)),
|
||||
["airflow", "configuration", "getint"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.configuration.conf.getint".to_string()),
|
||||
)),
|
||||
["airflow", "configuration", "has_option"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.configuration.conf.has_option".to_string()),
|
||||
)),
|
||||
["airflow", "configuration", "remove_option"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.configuration.conf.remove_option".to_string()),
|
||||
)),
|
||||
["airflow", "configuration", "as_dict"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.configuration.conf.as_dict".to_string()),
|
||||
)),
|
||||
["airflow", "configuration", "set"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.configuration.conf.set".to_string()),
|
||||
)),
|
||||
// airflow.contrib.*
|
||||
["airflow", "contrib", ..] => Some((qualname.to_string(), Replacement::None)),
|
||||
// airflow.metrics.validators
|
||||
["airflow", "metrics", "validators", "AllowListValidator"] => Some((
|
||||
qualname.to_string(),
|
||||
@@ -129,6 +226,85 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
|
||||
"airflow.metrics.validators.PatternBlockListValidator".to_string(),
|
||||
),
|
||||
)),
|
||||
// airflow.operators
|
||||
["airflow", "operators", "subdag", ..] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow.sensors.external_task.ExternalTaskSensorLink"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.sensors.external_task.ExternalDagLin".to_string()),
|
||||
)),
|
||||
["airflow", "operators", "bash_operator", "BashOperator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.operators.bash.BashOperator".to_string()),
|
||||
)),
|
||||
["airflow", "operators", "branch_operator", "BaseBranchOperator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.operators.branch.BaseBranchOperator".to_string()),
|
||||
)),
|
||||
["airflow", "operators", " dummy", "EmptyOperator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.operators.empty.EmptyOperator".to_string()),
|
||||
)),
|
||||
["airflow", "operators", "dummy", "DummyOperator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.operators.empty.EmptyOperator".to_string()),
|
||||
)),
|
||||
["airflow", "operators", "dummy_operator", "EmptyOperator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.operators.empty.EmptyOperator".to_string()),
|
||||
)),
|
||||
["airflow", "operators", "dummy_operator", "DummyOperator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.operators.empty.EmptyOperator".to_string()),
|
||||
)),
|
||||
["airflow", "operators", "email_operator", "EmailOperator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.operators.email.EmailOperator".to_string()),
|
||||
)),
|
||||
["airflow", "sensors", "base_sensor_operator", "BaseSensorOperator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.sensors.base.BaseSensorOperator".to_string()),
|
||||
)),
|
||||
["airflow", "sensors", "date_time_sensor", "DateTimeSensor"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.sensors.date_time.DateTimeSensor".to_string()),
|
||||
)),
|
||||
["airflow", "sensors", "external_task_sensor", "ExternalTaskMarker"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.sensors.external_task.ExternalTaskMarker".to_string(),
|
||||
),
|
||||
)),
|
||||
["airflow", "sensors", "external_task_sensor", "ExternalTaskSensor"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.sensors.external_task.ExternalTaskSensor".to_string(),
|
||||
),
|
||||
)),
|
||||
["airflow", "sensors", "external_task_sensor", "ExternalTaskSensorLink"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.sensors.external_task.ExternalTaskSensorLink".to_string(),
|
||||
),
|
||||
)),
|
||||
["airflow", "sensors", "time_delta_sensor", "TimeDeltaSensor"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.sensors.time_delta.TimeDeltaSensor".to_string()),
|
||||
)),
|
||||
// airflow.secrets
|
||||
["airflow", "secrets", "local_filesystem", "load_connections"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.secrets.local_filesystem.load_connections_dict".to_string(),
|
||||
),
|
||||
)),
|
||||
["airflow", "secrets", "local_filesystem", "get_connection"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.secrets.local_filesystem.load_connections_dict".to_string(),
|
||||
),
|
||||
)),
|
||||
// airflow.utils.dates
|
||||
["airflow", "utils", "dates", "date_range"] => Some((
|
||||
qualname.to_string(),
|
||||
@@ -158,6 +334,16 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
|
||||
qualname.to_string(),
|
||||
Replacement::Name("pendulum.today('UTC').add(days=-N, ...)".to_string()),
|
||||
)),
|
||||
|
||||
// airflow.utils.helpers
|
||||
["airflow", "utils", "helpers", "chain"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.models.baseoperator.chain".to_string()),
|
||||
)),
|
||||
["airflow", "utils", "helpers", "cross_downstream"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.models.baseoperator.cross_downstream".to_string()),
|
||||
)),
|
||||
// airflow.utils.state
|
||||
["airflow", "utils", "state", "SHUTDOWN"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
@@ -165,13 +351,42 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
|
||||
["airflow", "utils", "state", "terminating_states"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
// airflow.utils.trigger_rule
|
||||
["airflow", "utils", "trigger_rule", "TriggerRule", "DUMMY"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "trigger_rule", "TriggerRule", "NONE_FAILED_OR_SKIPPED"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
// airflow.uilts
|
||||
["airflow", "utils", "dag_cycle_tester", "test_cycle"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "decorators", "apply_defaults"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "decorators", "apply_defaults"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Message(
|
||||
"`apply_defaults` is now unconditionally done and can be safely removed."
|
||||
.to_string(),
|
||||
),
|
||||
)),
|
||||
// airflow.www
|
||||
["airflow", "www", "auth", "has_access"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.www.auth.has_access_*".to_string()),
|
||||
)),
|
||||
["airflow", "www", "utils", "get_sensitive_variables_fields"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.utils.log.secrets_masker.get_sensitive_variables_fields"
|
||||
.to_string(),
|
||||
),
|
||||
)),
|
||||
["airflow", "www", "utils", "should_hide_value_for_key"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.utils.log.secrets_masker.should_hide_value_for_key".to_string(),
|
||||
),
|
||||
)),
|
||||
_ => None,
|
||||
});
|
||||
if let Some((deprecated, replacement)) = result {
|
||||
|
||||
@@ -2,36 +2,87 @@
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
AIR302_args.py:6:39: AIR302 `schedule_interval` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
4 | DAG(dag_id="class_schedule", schedule="@hourly")
|
||||
5 |
|
||||
6 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
7 |
|
||||
8 | DAG(dag_id="class_timetable", timetable=NullTimetable())
|
||||
|
|
||||
|
||||
AIR302_args.py:8:31: AIR302 `timetable` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
6 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
|
||||
7 |
|
||||
8 | DAG(dag_id="class_timetable", timetable=NullTimetable())
|
||||
| ^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_args.py:16:6: AIR302 `schedule_interval` is removed in Airflow 3.0; use `schedule` instead
|
||||
AIR302_args.py:15:39: AIR302 `schedule_interval` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
16 | @dag(schedule_interval="0 * * * *")
|
||||
13 | DAG(dag_id="class_schedule", schedule="@hourly")
|
||||
14 |
|
||||
15 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
16 |
|
||||
17 | DAG(dag_id="class_timetable", timetable=NullTimetable())
|
||||
|
|
||||
|
||||
AIR302_args.py:17:31: AIR302 `timetable` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
15 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
|
||||
16 |
|
||||
17 | DAG(dag_id="class_timetable", timetable=NullTimetable())
|
||||
| ^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_args.py:24:34: AIR302 `sla_miss_callback` is removed in Airflow 3.0
|
||||
|
|
||||
24 | DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback)
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_args.py:32:6: AIR302 `schedule_interval` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
32 | @dag(schedule_interval="0 * * * *")
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
17 | def decorator_schedule_interval():
|
||||
18 | pass
|
||||
33 | def decorator_schedule_interval():
|
||||
34 | pass
|
||||
|
|
||||
|
||||
AIR302_args.py:21:6: AIR302 `timetable` is removed in Airflow 3.0; use `schedule` instead
|
||||
AIR302_args.py:37:6: AIR302 `timetable` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
21 | @dag(timetable=NullTimetable())
|
||||
37 | @dag(timetable=NullTimetable())
|
||||
| ^^^^^^^^^ AIR302
|
||||
22 | def decorator_timetable():
|
||||
23 | pass
|
||||
38 | def decorator_timetable():
|
||||
39 | pass
|
||||
|
|
||||
|
||||
AIR302_args.py:42:6: AIR302 `sla_miss_callback` is removed in Airflow 3.0
|
||||
|
|
||||
42 | @dag(sla_miss_callback=sla_callback)
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
43 | def decorator_sla_callback():
|
||||
44 | pass
|
||||
|
|
||||
|
||||
AIR302_args.py:50:39: AIR302 `execution_date` is removed in Airflow 3.0; use `logical_date` instead
|
||||
|
|
||||
48 | def decorator_deprecated_operator_args():
|
||||
49 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
|
||||
50 | task_id="trigger_dagrun_op1", execution_date="2024-12-04"
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
51 | )
|
||||
52 | trigger_dagrun_op2 = TriggerDagRunOperator(
|
||||
|
|
||||
|
||||
AIR302_args.py:53:39: AIR302 `execution_date` is removed in Airflow 3.0; use `logical_date` instead
|
||||
|
|
||||
51 | )
|
||||
52 | trigger_dagrun_op2 = TriggerDagRunOperator(
|
||||
53 | task_id="trigger_dagrun_op2", execution_date="2024-12-04"
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
54 | )
|
||||
|
|
||||
|
||||
AIR302_args.py:57:33: AIR302 `use_task_execution_day` is removed in Airflow 3.0; use `use_task_logical_date` instead
|
||||
|
|
||||
56 | branch_dt_op = datetime.BranchDateTimeOperator(
|
||||
57 | task_id="branch_dt_op", use_task_execution_day=True
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
58 | )
|
||||
59 | branch_dt_op2 = BranchDateTimeOperator(
|
||||
|
|
||||
|
||||
AIR302_args.py:60:34: AIR302 `use_task_execution_day` is removed in Airflow 3.0; use `use_task_logical_date` instead
|
||||
|
|
||||
58 | )
|
||||
59 | branch_dt_op2 = BranchDateTimeOperator(
|
||||
60 | task_id="branch_dt_op2", use_task_execution_day=True
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
61 | )
|
||||
|
|
||||
|
||||
@@ -2,156 +2,537 @@
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
AIR302_names.py:21:1: AIR302 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
|
||||
AIR302_names.py:52:1: AIR302 `airflow.PY36` is removed in Airflow 3.0; use `sys.version_info` instead
|
||||
|
|
||||
21 | TaskStateTrigger
|
||||
50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
|
||||
51 |
|
||||
52 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^ AIR302
|
||||
53 |
|
||||
54 | AWSAthenaHook
|
||||
|
|
||||
|
||||
AIR302_names.py:52:7: AIR302 `airflow.PY37` is removed in Airflow 3.0; use `sys.version_info` instead
|
||||
|
|
||||
50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
|
||||
51 |
|
||||
52 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^ AIR302
|
||||
53 |
|
||||
54 | AWSAthenaHook
|
||||
|
|
||||
|
||||
AIR302_names.py:52:13: AIR302 `airflow.PY38` is removed in Airflow 3.0; use `sys.version_info` instead
|
||||
|
|
||||
50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
|
||||
51 |
|
||||
52 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^ AIR302
|
||||
53 |
|
||||
54 | AWSAthenaHook
|
||||
|
|
||||
|
||||
AIR302_names.py:52:19: AIR302 `airflow.PY39` is removed in Airflow 3.0; use `sys.version_info` instead
|
||||
|
|
||||
50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
|
||||
51 |
|
||||
52 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^ AIR302
|
||||
53 |
|
||||
54 | AWSAthenaHook
|
||||
|
|
||||
|
||||
AIR302_names.py:52:25: AIR302 `airflow.PY310` is removed in Airflow 3.0; use `sys.version_info` instead
|
||||
|
|
||||
50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
|
||||
51 |
|
||||
52 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^^ AIR302
|
||||
53 |
|
||||
54 | AWSAthenaHook
|
||||
|
|
||||
|
||||
AIR302_names.py:52:32: AIR302 `airflow.PY311` is removed in Airflow 3.0; use `sys.version_info` instead
|
||||
|
|
||||
50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
|
||||
51 |
|
||||
52 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^^ AIR302
|
||||
53 |
|
||||
54 | AWSAthenaHook
|
||||
|
|
||||
|
||||
AIR302_names.py:52:39: AIR302 `airflow.PY312` is removed in Airflow 3.0; use `sys.version_info` instead
|
||||
|
|
||||
50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
|
||||
51 |
|
||||
52 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^^ AIR302
|
||||
53 |
|
||||
54 | AWSAthenaHook
|
||||
|
|
||||
|
||||
AIR302_names.py:54:1: AIR302 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0
|
||||
|
|
||||
52 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
53 |
|
||||
54 | AWSAthenaHook
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
55 | TaskStateTrigger
|
||||
|
|
||||
|
||||
AIR302_names.py:55:1: AIR302 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
|
||||
|
|
||||
54 | AWSAthenaHook
|
||||
55 | TaskStateTrigger
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
56 |
|
||||
57 | requires_access
|
||||
|
|
||||
|
||||
AIR302_names.py:24:1: AIR302 `airflow.www.auth.has_access` is removed in Airflow 3.0; use `airflow.www.auth.has_access_*` instead
|
||||
AIR302_names.py:57:1: AIR302 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0; use `airflow.api_connexion.security.requires_access_*` instead
|
||||
|
|
||||
24 | has_access
|
||||
| ^^^^^^^^^^ AIR302
|
||||
25 | requires_access
|
||||
|
|
||||
|
||||
AIR302_names.py:25:1: AIR302 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0; use `airflow.api_connexion.security.requires_access_*` instead
|
||||
|
|
||||
24 | has_access
|
||||
25 | requires_access
|
||||
55 | TaskStateTrigger
|
||||
56 |
|
||||
57 | requires_access
|
||||
| ^^^^^^^^^^^^^^^ AIR302
|
||||
26 |
|
||||
27 | AllowListValidator
|
||||
58 |
|
||||
59 | AllowListValidator
|
||||
|
|
||||
|
||||
AIR302_names.py:27:1: AIR302 `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0; use `airflow.metrics.validators.PatternAllowListValidator` instead
|
||||
AIR302_names.py:59:1: AIR302 `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0; use `airflow.metrics.validators.PatternAllowListValidator` instead
|
||||
|
|
||||
25 | requires_access
|
||||
26 |
|
||||
27 | AllowListValidator
|
||||
57 | requires_access
|
||||
58 |
|
||||
59 | AllowListValidator
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
28 | BlockListValidator
|
||||
60 | BlockListValidator
|
||||
|
|
||||
|
||||
AIR302_names.py:28:1: AIR302 `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0; use `airflow.metrics.validators.PatternBlockListValidator` instead
|
||||
AIR302_names.py:60:1: AIR302 `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0; use `airflow.metrics.validators.PatternBlockListValidator` instead
|
||||
|
|
||||
27 | AllowListValidator
|
||||
28 | BlockListValidator
|
||||
59 | AllowListValidator
|
||||
60 | BlockListValidator
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
29 |
|
||||
30 | dates.date_range
|
||||
61 |
|
||||
62 | SubDagOperator
|
||||
|
|
||||
|
||||
AIR302_names.py:30:7: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0; use `airflow.timetables.` instead
|
||||
AIR302_names.py:62:1: AIR302 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
|
||||
|
|
||||
28 | BlockListValidator
|
||||
29 |
|
||||
30 | dates.date_range
|
||||
60 | BlockListValidator
|
||||
61 |
|
||||
62 | SubDagOperator
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
63 |
|
||||
64 | dates.date_range
|
||||
|
|
||||
|
||||
AIR302_names.py:64:7: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0; use `airflow.timetables.` instead
|
||||
|
|
||||
62 | SubDagOperator
|
||||
63 |
|
||||
64 | dates.date_range
|
||||
| ^^^^^^^^^^ AIR302
|
||||
31 | dates.days_ago
|
||||
65 | dates.days_ago
|
||||
|
|
||||
|
||||
AIR302_names.py:31:7: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
AIR302_names.py:65:7: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
|
|
||||
30 | dates.date_range
|
||||
31 | dates.days_ago
|
||||
64 | dates.date_range
|
||||
65 | dates.days_ago
|
||||
| ^^^^^^^^ AIR302
|
||||
32 |
|
||||
33 | date_range
|
||||
66 |
|
||||
67 | date_range
|
||||
|
|
||||
|
||||
AIR302_names.py:33:1: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0; use `airflow.timetables.` instead
|
||||
AIR302_names.py:67:1: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0; use `airflow.timetables.` instead
|
||||
|
|
||||
31 | dates.days_ago
|
||||
32 |
|
||||
33 | date_range
|
||||
65 | dates.days_ago
|
||||
66 |
|
||||
67 | date_range
|
||||
| ^^^^^^^^^^ AIR302
|
||||
34 | days_ago
|
||||
35 | parse_execution_date
|
||||
68 | days_ago
|
||||
69 | parse_execution_date
|
||||
|
|
||||
|
||||
AIR302_names.py:34:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
AIR302_names.py:68:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
|
|
||||
33 | date_range
|
||||
34 | days_ago
|
||||
67 | date_range
|
||||
68 | days_ago
|
||||
| ^^^^^^^^ AIR302
|
||||
35 | parse_execution_date
|
||||
36 | round_time
|
||||
69 | parse_execution_date
|
||||
70 | round_time
|
||||
|
|
||||
|
||||
AIR302_names.py:35:1: AIR302 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
|
||||
AIR302_names.py:69:1: AIR302 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
33 | date_range
|
||||
34 | days_ago
|
||||
35 | parse_execution_date
|
||||
67 | date_range
|
||||
68 | days_ago
|
||||
69 | parse_execution_date
|
||||
| ^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
36 | round_time
|
||||
37 | scale_time_units
|
||||
70 | round_time
|
||||
71 | scale_time_units
|
||||
|
|
||||
|
||||
AIR302_names.py:36:1: AIR302 `airflow.utils.dates.round_time` is removed in Airflow 3.0
|
||||
AIR302_names.py:70:1: AIR302 `airflow.utils.dates.round_time` is removed in Airflow 3.0
|
||||
|
|
||||
34 | days_ago
|
||||
35 | parse_execution_date
|
||||
36 | round_time
|
||||
68 | days_ago
|
||||
69 | parse_execution_date
|
||||
70 | round_time
|
||||
| ^^^^^^^^^^ AIR302
|
||||
37 | scale_time_units
|
||||
38 | infer_time_unit
|
||||
71 | scale_time_units
|
||||
72 | infer_time_unit
|
||||
|
|
||||
|
||||
AIR302_names.py:37:1: AIR302 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
|
||||
AIR302_names.py:71:1: AIR302 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
|
||||
|
|
||||
35 | parse_execution_date
|
||||
36 | round_time
|
||||
37 | scale_time_units
|
||||
69 | parse_execution_date
|
||||
70 | round_time
|
||||
71 | scale_time_units
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
38 | infer_time_unit
|
||||
72 | infer_time_unit
|
||||
|
|
||||
|
||||
AIR302_names.py:38:1: AIR302 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
|
||||
AIR302_names.py:72:1: AIR302 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
|
||||
|
|
||||
36 | round_time
|
||||
37 | scale_time_units
|
||||
38 | infer_time_unit
|
||||
70 | round_time
|
||||
71 | scale_time_units
|
||||
72 | infer_time_unit
|
||||
| ^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_names.py:45:1: AIR302 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0
|
||||
AIR302_names.py:79:1: AIR302 `airflow.configuration.get` is removed in Airflow 3.0; use `airflow.configuration.conf.get` instead
|
||||
|
|
||||
43 | dates.datetime_to_nano
|
||||
44 |
|
||||
45 | TemporaryDirectory
|
||||
77 | dates.datetime_to_nano
|
||||
78 |
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
| ^^^ AIR302
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
|
|
||||
|
||||
AIR302_names.py:79:6: AIR302 `airflow.configuration.getboolean` is removed in Airflow 3.0; use `airflow.configuration.conf.getboolean` instead
|
||||
|
|
||||
77 | dates.datetime_to_nano
|
||||
78 |
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
| ^^^^^^^^^^ AIR302
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
|
|
||||
|
||||
AIR302_names.py:79:18: AIR302 `airflow.configuration.getfloat` is removed in Airflow 3.0; use `airflow.configuration.conf.getfloat` instead
|
||||
|
|
||||
77 | dates.datetime_to_nano
|
||||
78 |
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
| ^^^^^^^^ AIR302
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
|
|
||||
|
||||
AIR302_names.py:79:28: AIR302 `airflow.configuration.getint` is removed in Airflow 3.0; use `airflow.configuration.conf.getint` instead
|
||||
|
|
||||
77 | dates.datetime_to_nano
|
||||
78 |
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
| ^^^^^^ AIR302
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
|
|
||||
|
||||
AIR302_names.py:79:36: AIR302 `airflow.configuration.has_option` is removed in Airflow 3.0; use `airflow.configuration.conf.has_option` instead
|
||||
|
|
||||
77 | dates.datetime_to_nano
|
||||
78 |
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
| ^^^^^^^^^^ AIR302
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
|
|
||||
|
||||
AIR302_names.py:79:48: AIR302 `airflow.configuration.remove_option` is removed in Airflow 3.0; use `airflow.configuration.conf.remove_option` instead
|
||||
|
|
||||
77 | dates.datetime_to_nano
|
||||
78 |
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
|
|
||||
|
||||
AIR302_names.py:79:63: AIR302 `airflow.configuration.as_dict` is removed in Airflow 3.0; use `airflow.configuration.conf.as_dict` instead
|
||||
|
|
||||
77 | dates.datetime_to_nano
|
||||
78 |
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
| ^^^^^^^ AIR302
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
|
|
||||
|
||||
AIR302_names.py:79:72: AIR302 `airflow.configuration.set` is removed in Airflow 3.0; use `airflow.configuration.conf.set` instead
|
||||
|
|
||||
77 | dates.datetime_to_nano
|
||||
78 |
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
| ^^^ AIR302
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
|
|
||||
|
||||
AIR302_names.py:81:1: AIR302 `airflow.secrets.local_filesystem.get_connection` is removed in Airflow 3.0; use `airflow.secrets.local_filesystem.load_connections_dict` instead
|
||||
|
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_names.py:81:17: AIR302 `airflow.secrets.local_filesystem.load_connections` is removed in Airflow 3.0; use `airflow.secrets.local_filesystem.load_connections_dict` instead
|
||||
|
|
||||
79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
|
||||
80 |
|
||||
81 | get_connection, load_connections
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_names.py:84:1: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is removed in Airflow 3.0; use `airflow.sensors.external_task.ExternalTaskSensorLink` instead
|
||||
|
|
||||
84 | ExternalTaskSensorLink
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
85 | BashOperator
|
||||
86 | BaseBranchOperator
|
||||
|
|
||||
|
||||
AIR302_names.py:85:1: AIR302 `airflow.operators.bash_operator.BashOperator` is removed in Airflow 3.0; use `airflow.operators.bash.BashOperator` instead
|
||||
|
|
||||
84 | ExternalTaskSensorLink
|
||||
85 | BashOperator
|
||||
| ^^^^^^^^^^^^ AIR302
|
||||
86 | BaseBranchOperator
|
||||
87 | EmptyOperator, DummyOperator
|
||||
|
|
||||
|
||||
AIR302_names.py:86:1: AIR302 `airflow.operators.branch_operator.BaseBranchOperator` is removed in Airflow 3.0; use `airflow.operators.branch.BaseBranchOperator` instead
|
||||
|
|
||||
84 | ExternalTaskSensorLink
|
||||
85 | BashOperator
|
||||
86 | BaseBranchOperator
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
46 | mkdirs
|
||||
87 | EmptyOperator, DummyOperator
|
||||
88 | dummy_operator.EmptyOperator
|
||||
|
|
||||
|
||||
AIR302_names.py:46:1: AIR302 `airflow.utils.file.mkdirs` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
AIR302_names.py:87:16: AIR302 `airflow.operators.dummy.DummyOperator` is removed in Airflow 3.0; use `airflow.operators.empty.EmptyOperator` instead
|
||||
|
|
||||
45 | TemporaryDirectory
|
||||
46 | mkdirs
|
||||
| ^^^^^^ AIR302
|
||||
47 |
|
||||
48 | SHUTDOWN
|
||||
85 | BashOperator
|
||||
86 | BaseBranchOperator
|
||||
87 | EmptyOperator, DummyOperator
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
88 | dummy_operator.EmptyOperator
|
||||
89 | dummy_operator.DummyOperator
|
||||
|
|
||||
|
||||
AIR302_names.py:48:1: AIR302 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
|
||||
AIR302_names.py:88:16: AIR302 `airflow.operators.dummy_operator.EmptyOperator` is removed in Airflow 3.0; use `airflow.operators.empty.EmptyOperator` instead
|
||||
|
|
||||
46 | mkdirs
|
||||
47 |
|
||||
48 | SHUTDOWN
|
||||
| ^^^^^^^^ AIR302
|
||||
49 | terminating_states
|
||||
86 | BaseBranchOperator
|
||||
87 | EmptyOperator, DummyOperator
|
||||
88 | dummy_operator.EmptyOperator
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
89 | dummy_operator.DummyOperator
|
||||
90 | EmailOperator
|
||||
|
|
||||
|
||||
AIR302_names.py:49:1: AIR302 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
|
||||
AIR302_names.py:89:16: AIR302 `airflow.operators.dummy_operator.DummyOperator` is removed in Airflow 3.0; use `airflow.operators.empty.EmptyOperator` instead
|
||||
|
|
||||
48 | SHUTDOWN
|
||||
49 | terminating_states
|
||||
87 | EmptyOperator, DummyOperator
|
||||
88 | dummy_operator.EmptyOperator
|
||||
89 | dummy_operator.DummyOperator
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
90 | EmailOperator
|
||||
91 | BaseSensorOperator
|
||||
|
|
||||
|
||||
AIR302_names.py:90:1: AIR302 `airflow.operators.email_operator.EmailOperator` is removed in Airflow 3.0; use `airflow.operators.email.EmailOperator` instead
|
||||
|
|
||||
88 | dummy_operator.EmptyOperator
|
||||
89 | dummy_operator.DummyOperator
|
||||
90 | EmailOperator
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
91 | BaseSensorOperator
|
||||
92 | DateTimeSensor
|
||||
|
|
||||
|
||||
AIR302_names.py:91:1: AIR302 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0; use `airflow.sensors.base.BaseSensorOperator` instead
|
||||
|
|
||||
89 | dummy_operator.DummyOperator
|
||||
90 | EmailOperator
|
||||
91 | BaseSensorOperator
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
92 | DateTimeSensor
|
||||
93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
|
||||
|
|
||||
|
||||
AIR302_names.py:52:1: AIR302 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
|
||||
AIR302_names.py:92:1: AIR302 `airflow.sensors.date_time_sensor.DateTimeSensor` is removed in Airflow 3.0; use `airflow.sensors.date_time.DateTimeSensor` instead
|
||||
|
|
||||
52 | test_cycle
|
||||
| ^^^^^^^^^^ AIR302
|
||||
90 | EmailOperator
|
||||
91 | BaseSensorOperator
|
||||
92 | DateTimeSensor
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
|
||||
94 | TimeDeltaSensor
|
||||
|
|
||||
|
||||
AIR302_names.py:93:2: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskMarker` is removed in Airflow 3.0; use `airflow.sensors.external_task.ExternalTaskMarker` instead
|
||||
|
|
||||
91 | BaseSensorOperator
|
||||
92 | DateTimeSensor
|
||||
93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
94 | TimeDeltaSensor
|
||||
|
|
||||
|
||||
AIR302_names.py:93:22: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensor` is removed in Airflow 3.0; use `airflow.sensors.external_task.ExternalTaskSensor` instead
|
||||
|
|
||||
91 | BaseSensorOperator
|
||||
92 | DateTimeSensor
|
||||
93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
94 | TimeDeltaSensor
|
||||
|
|
||||
|
||||
AIR302_names.py:93:42: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is removed in Airflow 3.0; use `airflow.sensors.external_task.ExternalTaskSensorLink` instead
|
||||
|
|
||||
91 | BaseSensorOperator
|
||||
92 | DateTimeSensor
|
||||
93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
94 | TimeDeltaSensor
|
||||
|
|
||||
|
||||
AIR302_names.py:94:1: AIR302 `airflow.sensors.time_delta_sensor.TimeDeltaSensor` is removed in Airflow 3.0; use `airflow.sensors.time_delta.TimeDeltaSensor` instead
|
||||
|
|
||||
92 | DateTimeSensor
|
||||
93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
|
||||
94 | TimeDeltaSensor
|
||||
| ^^^^^^^^^^^^^^^ AIR302
|
||||
95 |
|
||||
96 | apply_defaults
|
||||
|
|
||||
|
||||
AIR302_names.py:96:1: AIR302 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0; `apply_defaults` is now unconditionally done and can be safely removed.
|
||||
|
|
||||
94 | TimeDeltaSensor
|
||||
95 |
|
||||
96 | apply_defaults
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
97 |
|
||||
98 | TemporaryDirectory
|
||||
|
|
||||
|
||||
AIR302_names.py:98:1: AIR302 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0
|
||||
|
|
||||
96 | apply_defaults
|
||||
97 |
|
||||
98 | TemporaryDirectory
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
99 | mkdirs
|
||||
|
|
||||
|
||||
AIR302_names.py:99:1: AIR302 `airflow.utils.file.mkdirs` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
|
|
||||
98 | TemporaryDirectory
|
||||
99 | mkdirs
|
||||
| ^^^^^^ AIR302
|
||||
100 |
|
||||
101 | chain
|
||||
|
|
||||
|
||||
AIR302_names.py:101:1: AIR302 `airflow.utils.helpers.chain` is removed in Airflow 3.0; use `airflow.models.baseoperator.chain` instead
|
||||
|
|
||||
99 | mkdirs
|
||||
100 |
|
||||
101 | chain
|
||||
| ^^^^^ AIR302
|
||||
102 | cross_downstream
|
||||
|
|
||||
|
||||
AIR302_names.py:102:1: AIR302 `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0; use `airflow.models.baseoperator.cross_downstream` instead
|
||||
|
|
||||
101 | chain
|
||||
102 | cross_downstream
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
103 |
|
||||
104 | SHUTDOWN
|
||||
|
|
||||
|
||||
AIR302_names.py:104:1: AIR302 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
|
||||
|
|
||||
102 | cross_downstream
|
||||
103 |
|
||||
104 | SHUTDOWN
|
||||
| ^^^^^^^^ AIR302
|
||||
105 | terminating_states
|
||||
|
|
||||
|
||||
AIR302_names.py:105:1: AIR302 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
|
||||
|
|
||||
104 | SHUTDOWN
|
||||
105 | terminating_states
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
106 |
|
||||
107 | TriggerRule.DUMMY
|
||||
|
|
||||
|
||||
AIR302_names.py:107:13: AIR302 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
|
||||
|
|
||||
105 | terminating_states
|
||||
106 |
|
||||
107 | TriggerRule.DUMMY
|
||||
| ^^^^^ AIR302
|
||||
108 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
||||
|
|
||||
|
||||
AIR302_names.py:108:13: AIR302 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
|
||||
|
|
||||
107 | TriggerRule.DUMMY
|
||||
108 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
109 |
|
||||
110 | test_cycle
|
||||
|
|
||||
|
||||
AIR302_names.py:110:1: AIR302 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
|
||||
|
|
||||
108 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
||||
109 |
|
||||
110 | test_cycle
|
||||
| ^^^^^^^^^^ AIR302
|
||||
111 |
|
||||
112 | has_access
|
||||
|
|
||||
|
||||
AIR302_names.py:112:1: AIR302 `airflow.www.auth.has_access` is removed in Airflow 3.0; use `airflow.www.auth.has_access_*` instead
|
||||
|
|
||||
110 | test_cycle
|
||||
111 |
|
||||
112 | has_access
|
||||
| ^^^^^^^^^^ AIR302
|
||||
113 | get_sensitive_variables_fields, should_hide_value_for_key
|
||||
|
|
||||
|
||||
AIR302_names.py:113:1: AIR302 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0; use `airflow.utils.log.secrets_masker.get_sensitive_variables_fields` instead
|
||||
|
|
||||
112 | has_access
|
||||
113 | get_sensitive_variables_fields, should_hide_value_for_key
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_names.py:113:33: AIR302 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0; use `airflow.utils.log.secrets_masker.should_hide_value_for_key` instead
|
||||
|
|
||||
112 | has_access
|
||||
113 | get_sensitive_variables_fields, should_hide_value_for_key
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
@@ -42,13 +42,18 @@ use crate::registry::Rule;
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct DuplicateTryBlockException {
|
||||
name: String,
|
||||
is_star: bool,
|
||||
}
|
||||
|
||||
impl Violation for DuplicateTryBlockException {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let DuplicateTryBlockException { name } = self;
|
||||
format!("try-except block with duplicate exception `{name}`")
|
||||
let DuplicateTryBlockException { name, is_star } = self;
|
||||
if *is_star {
|
||||
format!("try-except* block with duplicate exception `{name}`")
|
||||
} else {
|
||||
format!("try-except block with duplicate exception `{name}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,9 +212,15 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHand
|
||||
if checker.enabled(Rule::DuplicateTryBlockException) {
|
||||
for (name, exprs) in duplicates {
|
||||
for expr in exprs {
|
||||
let is_star = checker
|
||||
.semantic()
|
||||
.current_statement()
|
||||
.as_try_stmt()
|
||||
.is_some_and(|try_stmt| try_stmt.is_star);
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
DuplicateTryBlockException {
|
||||
name: name.segments().join("."),
|
||||
is_star,
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
|
||||
@@ -34,13 +34,18 @@ use crate::checkers::ast::Checker;
|
||||
/// ## References
|
||||
/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct ExceptWithEmptyTuple;
|
||||
pub(crate) struct ExceptWithEmptyTuple {
|
||||
is_star: bool,
|
||||
}
|
||||
|
||||
impl Violation for ExceptWithEmptyTuple {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Using `except ():` with an empty tuple does not catch anything; add exceptions to handle"
|
||||
.to_string()
|
||||
if self.is_star {
|
||||
"Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle".to_string()
|
||||
} else {
|
||||
"Using `except ():` with an empty tuple does not catch anything; add exceptions to handle".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +59,15 @@ pub(crate) fn except_with_empty_tuple(checker: &mut Checker, except_handler: &Ex
|
||||
let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if elts.is_empty() {
|
||||
let is_star = checker
|
||||
.semantic()
|
||||
.current_statement()
|
||||
.as_try_stmt()
|
||||
.is_some_and(|try_stmt| try_stmt.is_star);
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
ExceptWithEmptyTuple,
|
||||
ExceptWithEmptyTuple { is_star },
|
||||
except_handler.range(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -35,13 +35,20 @@ use crate::checkers::ast::Checker;
|
||||
/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
|
||||
/// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct ExceptWithNonExceptionClasses;
|
||||
pub(crate) struct ExceptWithNonExceptionClasses {
|
||||
is_star: bool,
|
||||
}
|
||||
|
||||
impl Violation for ExceptWithNonExceptionClasses {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`except` handlers should only be exception classes or tuples of exception classes"
|
||||
.to_string()
|
||||
if self.is_star {
|
||||
"`except*` handlers should only be exception classes or tuples of exception classes"
|
||||
.to_string()
|
||||
} else {
|
||||
"`except` handlers should only be exception classes or tuples of exception classes"
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +67,15 @@ pub(crate) fn except_with_non_exception_classes(
|
||||
expr,
|
||||
Expr::Subscript(_) | Expr::Attribute(_) | Expr::Name(_) | Expr::Call(_),
|
||||
) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(ExceptWithNonExceptionClasses, expr.range()));
|
||||
let is_star = checker
|
||||
.semantic()
|
||||
.current_statement()
|
||||
.as_try_stmt()
|
||||
.is_some_and(|try_stmt| try_stmt.is_star);
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
ExceptWithNonExceptionClasses { is_star },
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::{checkers::ast::Checker, fix::edits::add_argument};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `warnings.warn` calls without an explicit `stacklevel` keyword
|
||||
@@ -28,16 +28,26 @@ use crate::checkers::ast::Checker;
|
||||
/// warnings.warn("This is a warning", stacklevel=2)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe because it changes
|
||||
/// the behavior of the code. Moreover, the fix will assign
|
||||
/// a stacklevel of 2, while the user may wish to assign a
|
||||
/// higher stacklevel to address the diagnostic.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `warnings.warn`](https://docs.python.org/3/library/warnings.html#warnings.warn)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct NoExplicitStacklevel;
|
||||
|
||||
impl Violation for NoExplicitStacklevel {
|
||||
impl AlwaysFixableViolation for NoExplicitStacklevel {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"No explicit `stacklevel` keyword argument found".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Set `stacklevel=2`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// B028
|
||||
@@ -53,8 +63,16 @@ pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, call: &ast::ExprCall
|
||||
if call.arguments.find_keyword("stacklevel").is_some() {
|
||||
return;
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(NoExplicitStacklevel, call.func.range());
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(NoExplicitStacklevel, call.func.range()));
|
||||
let edit = add_argument(
|
||||
"stacklevel=2",
|
||||
&call.arguments,
|
||||
checker.comment_ranges(),
|
||||
checker.locator().contents(),
|
||||
);
|
||||
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit));
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -47,14 +47,22 @@ use crate::checkers::ast::Checker;
|
||||
/// ## References
|
||||
/// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct RaiseWithoutFromInsideExcept;
|
||||
pub(crate) struct RaiseWithoutFromInsideExcept {
|
||||
is_star: bool,
|
||||
}
|
||||
|
||||
impl Violation for RaiseWithoutFromInsideExcept {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... \
|
||||
from None` to distinguish them from errors in exception handling"
|
||||
.to_string()
|
||||
if self.is_star {
|
||||
"Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... \
|
||||
from None` to distinguish them from errors in exception handling"
|
||||
.to_string()
|
||||
} else {
|
||||
"Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... \
|
||||
from None` to distinguish them from errors in exception handling"
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +100,16 @@ pub(crate) fn raise_without_from_inside_except(
|
||||
}
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(RaiseWithoutFromInsideExcept, range));
|
||||
let is_star = checker
|
||||
.semantic()
|
||||
.current_statement()
|
||||
.as_try_stmt()
|
||||
.is_some_and(|try_stmt| try_stmt.is_star);
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
RaiseWithoutFromInsideExcept { is_star },
|
||||
range,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,3 +38,40 @@ B025.py:37:18: B025 try-except block with duplicate exception `TypeError`
|
||||
| ^^^^^^^^^ B025
|
||||
38 | a = 2
|
||||
|
|
||||
|
||||
B025.py:44:9: B025 try-except* block with duplicate exception `ValueError`
|
||||
|
|
||||
42 | except* ValueError:
|
||||
43 | a = 2
|
||||
44 | except* ValueError:
|
||||
| ^^^^^^^^^^ B025
|
||||
45 | a = 2
|
||||
|
|
||||
|
||||
B025.py:53:9: B025 try-except* block with duplicate exception `pickle.PickleError`
|
||||
|
|
||||
51 | except* ValueError:
|
||||
52 | a = 2
|
||||
53 | except* pickle.PickleError:
|
||||
| ^^^^^^^^^^^^^^^^^^ B025
|
||||
54 | a = 2
|
||||
|
|
||||
|
||||
B025.py:60:9: B025 try-except* block with duplicate exception `ValueError`
|
||||
|
|
||||
58 | except* (ValueError, TypeError):
|
||||
59 | a = 2
|
||||
60 | except* ValueError:
|
||||
| ^^^^^^^^^^ B025
|
||||
61 | a = 2
|
||||
62 | except* (OSError, TypeError):
|
||||
|
|
||||
|
||||
B025.py:62:19: B025 try-except* block with duplicate exception `TypeError`
|
||||
|
|
||||
60 | except* ValueError:
|
||||
61 | a = 2
|
||||
62 | except* (OSError, TypeError):
|
||||
| ^^^^^^^^^ B025
|
||||
63 | a = 2
|
||||
|
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
B028.py:8:1: B028 No explicit `stacklevel` keyword argument found
|
||||
B028.py:8:1: B028 [*] No explicit `stacklevel` keyword argument found
|
||||
|
|
||||
6 | """
|
||||
7 |
|
||||
@@ -11,8 +10,19 @@ B028.py:8:1: B028 No explicit `stacklevel` keyword argument found
|
||||
9 | warnings.warn(DeprecationWarning("test"), source=None)
|
||||
10 | warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2)
|
||||
|
|
||||
= help: Set `stacklevel=2`
|
||||
|
||||
B028.py:9:1: B028 No explicit `stacklevel` keyword argument found
|
||||
ℹ Unsafe fix
|
||||
5 5 | B028 - on lines 8 and 9
|
||||
6 6 | """
|
||||
7 7 |
|
||||
8 |-warnings.warn(DeprecationWarning("test"))
|
||||
8 |+warnings.warn(DeprecationWarning("test"), stacklevel=2)
|
||||
9 9 | warnings.warn(DeprecationWarning("test"), source=None)
|
||||
10 10 | warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2)
|
||||
11 11 | warnings.warn(DeprecationWarning("test"), stacklevel=1)
|
||||
|
||||
B028.py:9:1: B028 [*] No explicit `stacklevel` keyword argument found
|
||||
|
|
||||
8 | warnings.warn(DeprecationWarning("test"))
|
||||
9 | warnings.warn(DeprecationWarning("test"), source=None)
|
||||
@@ -20,3 +30,34 @@ B028.py:9:1: B028 No explicit `stacklevel` keyword argument found
|
||||
10 | warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2)
|
||||
11 | warnings.warn(DeprecationWarning("test"), stacklevel=1)
|
||||
|
|
||||
= help: Set `stacklevel=2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
6 6 | """
|
||||
7 7 |
|
||||
8 8 | warnings.warn(DeprecationWarning("test"))
|
||||
9 |-warnings.warn(DeprecationWarning("test"), source=None)
|
||||
10 9 | warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2)
|
||||
10 |+warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2)
|
||||
11 11 | warnings.warn(DeprecationWarning("test"), stacklevel=1)
|
||||
12 12 |
|
||||
13 13 | warnings.warn(
|
||||
|
||||
B028.py:13:1: B028 [*] No explicit `stacklevel` keyword argument found
|
||||
|
|
||||
11 | warnings.warn(DeprecationWarning("test"), stacklevel=1)
|
||||
12 |
|
||||
13 | warnings.warn(
|
||||
| ^^^^^^^^^^^^^ B028
|
||||
14 | DeprecationWarning("test"),
|
||||
15 | # some comments here
|
||||
|
|
||||
= help: Set `stacklevel=2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
13 13 | warnings.warn(
|
||||
14 14 | DeprecationWarning("test"),
|
||||
15 15 | # some comments here
|
||||
16 |- source = None # no trailing comma
|
||||
16 |+ source = None, stacklevel=2 # no trailing comma
|
||||
17 17 | )
|
||||
|
||||
@@ -20,4 +20,26 @@ B029.py:13:1: B029 Using `except ():` with an empty tuple does not catch anythin
|
||||
13 | / except () as e:
|
||||
14 | | pass
|
||||
| |________^ B029
|
||||
15 |
|
||||
16 | try:
|
||||
|
|
||||
|
||||
B029.py:18:1: B029 Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle
|
||||
|
|
||||
16 | try:
|
||||
17 | pass
|
||||
18 | / except* ():
|
||||
19 | | pass
|
||||
| |________^ B029
|
||||
20 |
|
||||
21 | try:
|
||||
|
|
||||
|
||||
B029.py:23:1: B029 Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle
|
||||
|
|
||||
21 | try:
|
||||
22 | pass
|
||||
23 | / except* () as e:
|
||||
24 | | pass
|
||||
| |________^ B029
|
||||
|
|
||||
|
||||
@@ -47,7 +47,7 @@ B030.py:33:29: B030 `except` handlers should only be exception classes or tuples
|
||||
34 | pass
|
||||
|
|
||||
|
||||
B030.py:39:28: B030 `except` handlers should only be exception classes or tuples of exception classes
|
||||
B030.py:39:28: B030 `except*` handlers should only be exception classes or tuples of exception classes
|
||||
|
|
||||
37 | try:
|
||||
38 | pass
|
||||
@@ -64,3 +64,39 @@ B030.py:131:8: B030 `except` handlers should only be exception classes or tuples
|
||||
| ^^^^^^^^^^^^^^^ B030
|
||||
132 | pass
|
||||
|
|
||||
|
||||
B030.py:136:9: B030 `except*` handlers should only be exception classes or tuples of exception classes
|
||||
|
|
||||
134 | try:
|
||||
135 | pass
|
||||
136 | except* 1: # Error
|
||||
| ^ B030
|
||||
137 | pass
|
||||
|
|
||||
|
||||
B030.py:141:10: B030 `except*` handlers should only be exception classes or tuples of exception classes
|
||||
|
|
||||
139 | try:
|
||||
140 | pass
|
||||
141 | except* (1, ValueError): # Error
|
||||
| ^ B030
|
||||
142 | pass
|
||||
|
|
||||
|
||||
B030.py:146:22: B030 `except*` handlers should only be exception classes or tuples of exception classes
|
||||
|
|
||||
144 | try:
|
||||
145 | pass
|
||||
146 | except* (ValueError, (RuntimeError, (KeyError, TypeError))): # Error
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B030
|
||||
147 | pass
|
||||
|
|
||||
|
||||
B030.py:151:9: B030 `except*` handlers should only be exception classes or tuples of exception classes
|
||||
|
|
||||
149 | try:
|
||||
150 | pass
|
||||
151 | except* (a, b) * (c, d): # B030
|
||||
| ^^^^^^^^^^^^^^^ B030
|
||||
152 | pass
|
||||
|
|
||||
|
||||
@@ -66,4 +66,66 @@ B904.py:73:13: B904 Within an `except` clause, raise exceptions with `raise ...
|
||||
72 | case 0:
|
||||
73 | raise RuntimeError("boom!")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904
|
||||
74 |
|
||||
75 | try:
|
||||
|
|
||||
|
||||
B904.py:79:9: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
||||
|
|
||||
77 | except* Exception as e:
|
||||
78 | if ...:
|
||||
79 | raise RuntimeError("boom!")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904
|
||||
80 | else:
|
||||
81 | raise RuntimeError("bang!")
|
||||
|
|
||||
|
||||
B904.py:81:9: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
||||
|
|
||||
79 | raise RuntimeError("boom!")
|
||||
80 | else:
|
||||
81 | raise RuntimeError("bang!")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904
|
||||
82 |
|
||||
83 | try:
|
||||
|
|
||||
|
||||
B904.py:87:9: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
||||
|
|
||||
85 | except* ValueError:
|
||||
86 | if "abc":
|
||||
87 | raise TypeError
|
||||
| ^^^^^^^^^^^^^^^ B904
|
||||
88 | raise UserWarning
|
||||
89 | except* AssertionError:
|
||||
|
|
||||
|
||||
B904.py:88:5: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
||||
|
|
||||
86 | if "abc":
|
||||
87 | raise TypeError
|
||||
88 | raise UserWarning
|
||||
| ^^^^^^^^^^^^^^^^^ B904
|
||||
89 | except* AssertionError:
|
||||
90 | raise # Bare `raise` should not be an error
|
||||
|
|
||||
|
||||
B904.py:93:5: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
||||
|
|
||||
91 | except* Exception as err:
|
||||
92 | assert err
|
||||
93 | raise Exception("No cause here...")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904
|
||||
94 | except* BaseException as err:
|
||||
95 | raise err
|
||||
|
|
||||
|
||||
B904.py:97:5: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
||||
|
|
||||
95 | raise err
|
||||
96 | except* BaseException as err:
|
||||
97 | raise some_other_err
|
||||
| ^^^^^^^^^^^^^^^^^^^^ B904
|
||||
98 | finally:
|
||||
99 | raise Exception("Nothing to chain from, so no warning here")
|
||||
|
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user