Compare commits
56 Commits
david/fix-
...
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 | ||
|
|
b0e26e6fc8 | ||
|
|
e9941cd714 | ||
|
|
43bf1a8907 | ||
|
|
fda8b1f884 | ||
|
|
2d3f557875 | ||
|
|
bd27bfab5d | ||
|
|
155d34bbb9 | ||
|
|
04c887c8fc | ||
|
|
af43bd4b0f | ||
|
|
614917769e | ||
|
|
8b23086eac |
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]
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Avoid deprecated values (`AIR302`) ([#14582](https://github.com/astral-sh/ruff/pull/14582))
|
||||
- \[`airflow`\] Extend removed names for `AIR302` ([#14734](https://github.com/astral-sh/ruff/pull/14734))
|
||||
- \[`ruff`\] Extend `unnecessary-regular-expression` to non-literal strings (`RUF055`) ([#14679](https://github.com/astral-sh/ruff/pull/14679))
|
||||
- \[`ruff`\] Implement `used-dummy-variable` (`RUF052`) ([#14611](https://github.com/astral-sh/ruff/pull/14611))
|
||||
- \[`ruff`\] Implement `unnecessary-cast-to-int` (`RUF046`) ([#14697](https://github.com/astral-sh/ruff/pull/14697))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`airflow`\] Check `AIR001` from builtin or providers `operators` module ([#14631](https://github.com/astral-sh/ruff/pull/14631))
|
||||
- \[`flake8-pytest-style`\] Remove `@` in `pytest.mark.parametrize` rule messages ([#14770](https://github.com/astral-sh/ruff/pull/14770))
|
||||
- \[`pandas-vet`\] Skip rules if the `panda` module hasn't been seen ([#14671](https://github.com/astral-sh/ruff/pull/14671))
|
||||
- \[`pylint`\] Fix false negatives for `ascii` and `sorted` in `len-as-condition` (`PLC1802`) ([#14692](https://github.com/astral-sh/ruff/pull/14692))
|
||||
- \[`refurb`\] Guard `hashlib` imports and mark `hashlib-digest-hex` fix as safe (`FURB181`) ([#14694](https://github.com/astral-sh/ruff/pull/14694))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-import-conventions`\] Improve syntax check for aliases supplied in configuration for `unconventional-import-alias` (`ICN001`) ([#14745](https://github.com/astral-sh/ruff/pull/14745))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Revert: [pyflakes] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) (#14615) ([#14726](https://github.com/astral-sh/ruff/pull/14726))
|
||||
- \[`pep8-naming`\] Avoid false positive for `class Bar(type(foo))` (`N804`) ([#14683](https://github.com/astral-sh/ruff/pull/14683))
|
||||
- \[`pycodestyle`\] Handle f-strings properly for `invalid-escape-sequence` (`W605`) ([#14748](https://github.com/astral-sh/ruff/pull/14748))
|
||||
- \[`pylint`\] Ignore `@overload` in `PLR0904` ([#14730](https://github.com/astral-sh/ruff/pull/14730))
|
||||
- \[`refurb`\] Handle non-finite decimals in `verbose-decimal-constructor` (`FURB157`) ([#14596](https://github.com/astral-sh/ruff/pull/14596))
|
||||
- \[`ruff`\] Avoid emitting `assignment-in-assert` when all references to the assigned variable are themselves inside `assert`s (`RUF018`) ([#14661](https://github.com/astral-sh/ruff/pull/14661))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improve docs for `flake8-use-pathlib` rules ([#14741](https://github.com/astral-sh/ruff/pull/14741))
|
||||
- Improve error messages and docs for `flake8-comprehensions` rules ([#14729](https://github.com/astral-sh/ruff/pull/14729))
|
||||
- \[`flake8-type-checking`\] Expands `TC006` docs to better explain itself ([#14749](https://github.com/astral-sh/ruff/pull/14749))
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Preview features
|
||||
|
||||
89
Cargo.lock
generated
89
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",
|
||||
]
|
||||
@@ -2513,7 +2517,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -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",
|
||||
@@ -2732,7 +2736,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -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",
|
||||
]
|
||||
@@ -3047,7 +3051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -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]]
|
||||
|
||||
10
README.md
10
README.md
@@ -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
|
||||
|
||||
@@ -136,8 +140,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.8.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.1/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.8.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.2/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +174,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.1
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: 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 }
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Any
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Any` is a way to name the Any type.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
x: Any = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
```
|
||||
|
||||
## Aliased to a different name
|
||||
|
||||
If you alias `typing.Any` to another name, we still recognize that as a spelling of the Any type.
|
||||
|
||||
```py
|
||||
from typing import Any as RenamedAny
|
||||
|
||||
x: RenamedAny = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
```
|
||||
|
||||
## Shadowed class
|
||||
|
||||
If you define your own class named `Any`, using that in a type expression refers to your class, and
|
||||
isn't a spelling of the Any type.
|
||||
|
||||
```py
|
||||
class Any:
|
||||
pass
|
||||
|
||||
x: Any
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
|
||||
# This verifies that we're not accidentally seeing typing.Any, since str is assignable
|
||||
# to that but not to our locally defined class.
|
||||
y: Any = "not an Any" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Subclass
|
||||
|
||||
The spec allows you to define subclasses of `Any`.
|
||||
|
||||
TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The
|
||||
assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment
|
||||
to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore
|
||||
assignable to `int`.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Subclass(Any):
|
||||
pass
|
||||
|
||||
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
|
||||
|
||||
x: Subclass = 1 # error: [invalid-assignment]
|
||||
# TODO: no diagnostic
|
||||
y: int = Subclass() # error: [invalid-assignment]
|
||||
|
||||
def f() -> Subclass:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Subclass
|
||||
```
|
||||
@@ -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,219 @@
|
||||
# Length (`len()`)
|
||||
|
||||
## Literal and constructed iterables
|
||||
|
||||
### Strings and bytes literals
|
||||
|
||||
```py
|
||||
reveal_type(len("no\rmal")) # revealed: Literal[6]
|
||||
reveal_type(len(r"aw stri\ng")) # revealed: Literal[10]
|
||||
reveal_type(len(r"conca\t" "ena\tion")) # revealed: Literal[14]
|
||||
reveal_type(len(b"ytes lite" rb"al")) # revealed: Literal[11]
|
||||
reveal_type(len("𝒰𝕹🄸©🕲𝕕ℇ")) # revealed: Literal[7]
|
||||
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
r"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
b"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
rb"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Tuples
|
||||
|
||||
```py
|
||||
reveal_type(len(())) # revealed: Literal[0]
|
||||
reveal_type(len((1,))) # revealed: Literal[1]
|
||||
reveal_type(len((1, 2))) # revealed: Literal[2]
|
||||
|
||||
# TODO: Handle constructor calls
|
||||
reveal_type(len(tuple())) # revealed: int
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[],))) # revealed: Literal[1]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[1]
|
||||
reveal_type( # revealed: Literal[2]
|
||||
len(
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[2]
|
||||
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[], *{}))) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
### Lists, sets and dictionaries
|
||||
|
||||
```py
|
||||
reveal_type(len([])) # revealed: int
|
||||
reveal_type(len([1])) # revealed: int
|
||||
reveal_type(len([1, 2])) # revealed: int
|
||||
reveal_type(len([*{}, *dict()])) # revealed: int
|
||||
|
||||
reveal_type(len({})) # revealed: int
|
||||
reveal_type(len({**{}})) # revealed: int
|
||||
reveal_type(len({**{}, **{}})) # revealed: int
|
||||
|
||||
reveal_type(len({1})) # revealed: int
|
||||
reveal_type(len({1, 2})) # revealed: int
|
||||
reveal_type(len({*[], 2})) # revealed: int
|
||||
|
||||
reveal_type(len(list())) # revealed: int
|
||||
reveal_type(len(set())) # revealed: int
|
||||
reveal_type(len(dict())) # revealed: int
|
||||
reveal_type(len(frozenset())) # revealed: int
|
||||
```
|
||||
|
||||
## `__len__`
|
||||
|
||||
The returned value of `__len__` is implicitly and recursively converted to `int`.
|
||||
|
||||
### Literal integers
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Zero:
|
||||
def __len__(self) -> Literal[0]: ...
|
||||
|
||||
class ZeroOrOne:
|
||||
def __len__(self) -> Literal[0, 1]: ...
|
||||
|
||||
class ZeroOrTrue:
|
||||
def __len__(self) -> Literal[0, True]: ...
|
||||
|
||||
class OneOrFalse:
|
||||
def __len__(self) -> Literal[1] | Literal[False]: ...
|
||||
|
||||
class OneOrFoo:
|
||||
def __len__(self) -> Literal[1, "foo"]: ...
|
||||
|
||||
class ZeroOrStr:
|
||||
def __len__(self) -> Literal[0] | str: ...
|
||||
|
||||
reveal_type(len(Zero())) # revealed: Literal[0]
|
||||
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(OneOrFoo())) # revealed: int
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(ZeroOrStr())) # revealed: int
|
||||
```
|
||||
|
||||
### Literal booleans
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class LiteralTrue:
|
||||
def __len__(self) -> Literal[True]: ...
|
||||
|
||||
class LiteralFalse:
|
||||
def __len__(self) -> Literal[False]: ...
|
||||
|
||||
reveal_type(len(LiteralTrue())) # revealed: Literal[1]
|
||||
reveal_type(len(LiteralFalse())) # revealed: Literal[0]
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
```py
|
||||
from enum import Enum, auto
|
||||
from typing import Literal
|
||||
|
||||
class SomeEnum(Enum):
|
||||
AUTO = auto()
|
||||
INT = 2
|
||||
STR = "4"
|
||||
TUPLE = (8, "16")
|
||||
INT_2 = 3_2
|
||||
|
||||
class Auto:
|
||||
def __len__(self) -> Literal[SomeEnum.AUTO]: ...
|
||||
|
||||
class Int:
|
||||
def __len__(self) -> Literal[SomeEnum.INT]: ...
|
||||
|
||||
class Str:
|
||||
def __len__(self) -> Literal[SomeEnum.STR]: ...
|
||||
|
||||
class Tuple:
|
||||
def __len__(self) -> Literal[SomeEnum.TUPLE]: ...
|
||||
|
||||
class IntUnion:
|
||||
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
|
||||
|
||||
reveal_type(len(Auto())) # revealed: int
|
||||
reveal_type(len(Int())) # revealed: Literal[2]
|
||||
reveal_type(len(Str())) # revealed: int
|
||||
reveal_type(len(Tuple())) # revealed: int
|
||||
reveal_type(len(IntUnion())) # revealed: Literal[2, 32]
|
||||
```
|
||||
|
||||
### Negative integers
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Negative:
|
||||
def __len__(self) -> Literal[-1]: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(Negative())) # revealed: int
|
||||
```
|
||||
|
||||
### Wrong signature
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class SecondOptionalArgument:
|
||||
def __len__(self, v: int = 0) -> Literal[0]: ...
|
||||
|
||||
class SecondRequiredArgument:
|
||||
def __len__(self, v: int) -> Literal[1]: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### No `__len__`
|
||||
|
||||
```py
|
||||
class NoDunderLen:
|
||||
pass
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(NoDunderLen())) # revealed: int
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,93 @@
|
||||
# Syntax errors
|
||||
|
||||
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
|
||||
error and including the text value of the keyword to create the `Identifier` node.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
#### 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"
|
||||
match while:
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `case` used when not defined"
|
||||
case in:
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
pass
|
||||
```
|
||||
|
||||
### Attribute expression
|
||||
|
||||
```py
|
||||
# TODO: Check when support for attribute expressions is added
|
||||
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
for x in foo.pass:
|
||||
pass
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -267,3 +267,42 @@ reveal_type(b) # revealed: LiteralString
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
### Unicode
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "é"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Unicode escape (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "\u9E6C"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Unicode escape (2)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "\U0010FFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Surrogates
|
||||
|
||||
```py
|
||||
(a, b) = "\uD800\uDFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
```
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::lint::RuleSelection;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
@@ -5,18 +6,25 @@ 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)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
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};
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct TestDb {
|
||||
@@ -24,7 +32,8 @@ pub(crate) mod tests {
|
||||
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 {
|
||||
@@ -33,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())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,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]
|
||||
@@ -108,4 +122,66 @@ pub(crate) mod tests {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestDbBuilder<'a> {
|
||||
/// Target Python version
|
||||
python_version: PythonVersion,
|
||||
/// Path to a custom typeshed directory
|
||||
custom_typeshed: Option<SystemPathBuf>,
|
||||
/// Path and content pairs for files that should be present
|
||||
files: Vec<(&'a str, &'a str)>,
|
||||
}
|
||||
|
||||
impl<'a> TestDbBuilder<'a> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
python_version: PythonVersion::default(),
|
||||
custom_typeshed: None,
|
||||
files: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_python_version(mut self, version: PythonVersion) -> Self {
|
||||
self.python_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_custom_typeshed(mut self, path: &str) -> Self {
|
||||
self.custom_typeshed = Some(SystemPathBuf::from(path));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
|
||||
self.files.push((path, content));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> anyhow::Result<TestDb> {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system().create_directory_all(&src_root)?;
|
||||
|
||||
db.write_files(self.files)
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(src_root);
|
||||
search_paths.custom_typeshed = self.custom_typeshed;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: self.python_version,
|
||||
search_paths,
|
||||
},
|
||||
)
|
||||
.context("Failed to configure Program settings")?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn setup_db() -> TestDb {
|
||||
TestDbBuilder::new().build().expect("valid TestDb setup")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -166,31 +166,15 @@ impl_binding_has_ty!(ast::ParameterWithDefault);
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{HasTy, ProgramSettings, SemanticModel};
|
||||
|
||||
fn setup_db<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<TestDb> {
|
||||
let mut db = TestDb::new();
|
||||
db.write_files(files)?;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(SystemPathBuf::from("/src")),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
use crate::db::tests::TestDbBuilder;
|
||||
use crate::{HasTy, SemanticModel};
|
||||
|
||||
#[test]
|
||||
fn function_ty() -> anyhow::Result<()> {
|
||||
let db = setup_db([("/src/foo.py", "def test(): pass")])?;
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "def test(): pass")
|
||||
.build()?;
|
||||
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
@@ -207,7 +191,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn class_ty() -> anyhow::Result<()> {
|
||||
let db = setup_db([("/src/foo.py", "class Test: pass")])?;
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "class Test: pass")
|
||||
.build()?;
|
||||
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
@@ -224,10 +210,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn alias_ty() -> anyhow::Result<()> {
|
||||
let db = setup_db([
|
||||
("/src/foo.py", "class Test: pass"),
|
||||
("/src/bar.py", "from foo import Test"),
|
||||
])?;
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "class Test: pass")
|
||||
.with_file("/src/bar.py", "from foo import Test")
|
||||
.build()?;
|
||||
|
||||
let bar = system_path_to_file(&db, "/src/bar.py").unwrap();
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ impl<'db> Symbol<'db> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::tests::setup_db;
|
||||
use crate::db::tests::setup_db;
|
||||
|
||||
#[test]
|
||||
fn test_symbol_or_fall_back_to() {
|
||||
|
||||
@@ -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,10 +26,10 @@ 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};
|
||||
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
|
||||
|
||||
mod builder;
|
||||
mod diagnostic;
|
||||
@@ -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.
|
||||
@@ -1027,9 +1040,24 @@ impl<'db> Type<'db> {
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::KnownInstance(_) => true,
|
||||
Type::SubclassOf(subclass_of) => subclass_of.class.is_fully_static(db),
|
||||
Type::ClassLiteral(class_literal) => class_literal.class.is_fully_static(db),
|
||||
Type::Instance(instance) => instance.class.is_fully_static(db),
|
||||
Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) => {
|
||||
// TODO: Ideally, we would iterate over the MRO of the class, check if all
|
||||
// bases are fully static, and only return `true` if that is the case.
|
||||
//
|
||||
// This does not work yet, because we currently infer `Unknown` for some
|
||||
// generic base classes that we don't understand yet. For example, `str`
|
||||
// is defined as `class str(Sequence[str])` in typeshed and we currently
|
||||
// compute its MRO as `(str, Unknown, object)`. This would make us think
|
||||
// that `str` is a gradual type, which causes all sorts of downstream
|
||||
// issues because it does not participate in equivalence/subtyping etc.
|
||||
//
|
||||
// Another problem is that we run into problems if we eagerly query the
|
||||
// MRO of class literals here. I have not fully investigated this, but
|
||||
// iterating over the MRO alone, without even acting on it, causes us to
|
||||
// infer `Unknown` for many classes.
|
||||
|
||||
true
|
||||
}
|
||||
Type::Union(union) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
@@ -1402,21 +1430,76 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the type of `len()` on a type if it is known more precisely than `int`,
|
||||
/// or `None` otherwise.
|
||||
///
|
||||
/// In the second case, the return type of `len()` in `typeshed` (`int`)
|
||||
/// is used as a fallback.
|
||||
fn len(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
|
||||
match ty {
|
||||
// TODO: Emit diagnostic for non-integers and negative integers
|
||||
Type::IntLiteral(value) => (value >= 0).then_some(ty),
|
||||
Type::BooleanLiteral(value) => Some(Type::IntLiteral(value.into())),
|
||||
Type::Union(union) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in union.elements(db) {
|
||||
builder = builder.add(non_negative_int_literal(db, *element)?);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let usize_len = match self {
|
||||
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
|
||||
Type::StringLiteral(string) => Some(string.python_len(db)),
|
||||
Type::Tuple(tuple) => Some(tuple.len(db)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(usize_len) = usize_len {
|
||||
return usize_len.try_into().ok().map(Type::IntLiteral);
|
||||
}
|
||||
|
||||
let return_ty = match self.call_dunder(db, "__len__", &[*self]) {
|
||||
// TODO: emit a diagnostic
|
||||
CallDunderResult::MethodNotAvailable => return None,
|
||||
|
||||
CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => {
|
||||
outcome.return_ty(db)?
|
||||
}
|
||||
};
|
||||
|
||||
non_negative_int_literal(db, return_ty)
|
||||
}
|
||||
|
||||
/// Return the outcome of calling an object of this type.
|
||||
#[must_use]
|
||||
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> {
|
||||
match self {
|
||||
// TODO validate typed call arguments vs callable signature
|
||||
Type::FunctionLiteral(function_type) => {
|
||||
if function_type.is_known(db, KnownFunction::RevealType) {
|
||||
CallOutcome::revealed(
|
||||
function_type.signature(db).return_ty,
|
||||
*arg_types.first().unwrap_or(&Type::Unknown),
|
||||
)
|
||||
} else {
|
||||
CallOutcome::callable(function_type.signature(db).return_ty)
|
||||
Type::FunctionLiteral(function_type) => match function_type.known(db) {
|
||||
Some(KnownFunction::RevealType) => CallOutcome::revealed(
|
||||
function_type.signature(db).return_ty,
|
||||
*arg_types.first().unwrap_or(&Type::Unknown),
|
||||
),
|
||||
|
||||
Some(KnownFunction::Len) => {
|
||||
let normal_return_ty = function_type.signature(db).return_ty;
|
||||
|
||||
let [only_arg] = arg_types else {
|
||||
// TODO: Emit a diagnostic
|
||||
return CallOutcome::callable(normal_return_ty);
|
||||
};
|
||||
let len_ty = only_arg.len(db);
|
||||
|
||||
CallOutcome::callable(len_ty.unwrap_or(normal_return_ty))
|
||||
}
|
||||
}
|
||||
|
||||
_ => CallOutcome::callable(function_type.signature(db).return_ty),
|
||||
},
|
||||
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
@@ -1619,6 +1702,7 @@ impl<'db> Type<'db> {
|
||||
Type::Never
|
||||
}
|
||||
Type::KnownInstance(KnownInstanceType::LiteralString) => Type::LiteralString,
|
||||
Type::KnownInstance(KnownInstanceType::Any) => Type::Any,
|
||||
_ => todo_type!(),
|
||||
}
|
||||
}
|
||||
@@ -1826,13 +1910,13 @@ impl<'db> KnownClass {
|
||||
}
|
||||
|
||||
pub fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
|
||||
core_module_symbol(db, self.canonical_module(), self.as_str())
|
||||
core_module_symbol(db, self.canonical_module(db), self.as_str())
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Unknown)
|
||||
}
|
||||
|
||||
/// Return the module in which we should look up the definition for this class
|
||||
pub(crate) const fn canonical_module(self) -> CoreStdlibModule {
|
||||
pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule {
|
||||
match self {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
@@ -1850,10 +1934,18 @@ impl<'db> KnownClass {
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
|
||||
Self::NoneType => CoreStdlibModule::Typeshed,
|
||||
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing,
|
||||
// TODO when we understand sys.version_info, we will need an explicit fallback here,
|
||||
// because typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
|
||||
// singleton, but not for `typing._NoDefaultType`
|
||||
Self::NoDefaultType => CoreStdlibModule::TypingExtensions,
|
||||
Self::NoDefaultType => {
|
||||
let python_version = Program::get(db).target_version(db);
|
||||
|
||||
// typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
|
||||
// singleton, but not for `typing._NoDefaultType`. So we need to switch
|
||||
// to `typing._NoDefaultType` for newer versions:
|
||||
if python_version >= PythonVersion::PY313 {
|
||||
CoreStdlibModule::Typing
|
||||
} else {
|
||||
CoreStdlibModule::TypingExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1913,11 +2005,11 @@ impl<'db> KnownClass {
|
||||
};
|
||||
|
||||
let module = file_to_module(db, file)?;
|
||||
candidate.check_module(&module).then_some(candidate)
|
||||
candidate.check_module(db, &module).then_some(candidate)
|
||||
}
|
||||
|
||||
/// Return `true` if the module of `self` matches `module_name`
|
||||
fn check_module(self, module: &Module) -> bool {
|
||||
fn check_module(self, db: &'db dyn Db, module: &Module) -> bool {
|
||||
if !module.search_path().is_standard_library() {
|
||||
return false;
|
||||
}
|
||||
@@ -1937,7 +2029,7 @@ impl<'db> KnownClass {
|
||||
| Self::GenericAlias
|
||||
| Self::ModuleType
|
||||
| Self::VersionInfo
|
||||
| Self::FunctionType => module.name() == self.canonical_module().as_str(),
|
||||
| Self::FunctionType => module.name() == self.canonical_module(db).as_str(),
|
||||
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
|
||||
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
|
||||
matches!(module.name().as_str(), "typing" | "typing_extensions")
|
||||
@@ -1961,6 +2053,8 @@ pub enum KnownInstanceType<'db> {
|
||||
NoReturn,
|
||||
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
|
||||
Never,
|
||||
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
|
||||
Any,
|
||||
/// A single instance of `typing.TypeVar`
|
||||
TypeVar(TypeVarInstance<'db>),
|
||||
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
|
||||
@@ -1978,6 +2072,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::TypeVar(_) => "TypeVar",
|
||||
Self::NoReturn => "NoReturn",
|
||||
Self::Never => "Never",
|
||||
Self::Any => "Any",
|
||||
Self::TypeAliasType(_) => "TypeAliasType",
|
||||
}
|
||||
}
|
||||
@@ -1992,6 +2087,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::Union
|
||||
| Self::NoReturn
|
||||
| Self::Never
|
||||
| Self::Any
|
||||
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
|
||||
}
|
||||
}
|
||||
@@ -2005,6 +2101,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::Union => "typing.Union",
|
||||
Self::NoReturn => "typing.NoReturn",
|
||||
Self::Never => "typing.Never",
|
||||
Self::Any => "typing.Any",
|
||||
Self::TypeVar(typevar) => typevar.name(db),
|
||||
Self::TypeAliasType(_) => "typing.TypeAliasType",
|
||||
}
|
||||
@@ -2019,6 +2116,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::Union => KnownClass::SpecialForm,
|
||||
Self::NoReturn => KnownClass::SpecialForm,
|
||||
Self::Never => KnownClass::SpecialForm,
|
||||
Self::Any => KnownClass::Object,
|
||||
Self::TypeVar(_) => KnownClass::TypeVar,
|
||||
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
|
||||
}
|
||||
@@ -2038,6 +2136,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
return None;
|
||||
}
|
||||
match (module.name().as_str(), instance_name) {
|
||||
("typing", "Any") => Some(Self::Any),
|
||||
("typing" | "typing_extensions", "Literal") => Some(Self::Literal),
|
||||
("typing" | "typing_extensions", "LiteralString") => Some(Self::LiteralString),
|
||||
("typing" | "typing_extensions", "Optional") => Some(Self::Optional),
|
||||
@@ -2202,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)
|
||||
@@ -2217,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),
|
||||
@@ -2233,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),
|
||||
@@ -2248,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)
|
||||
@@ -2276,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)
|
||||
@@ -2574,13 +2674,15 @@ pub enum KnownFunction {
|
||||
ConstraintFunction(KnownConstraintFunction),
|
||||
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
||||
RevealType,
|
||||
/// `builtins.len`
|
||||
Len,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
|
||||
match self {
|
||||
Self::ConstraintFunction(f) => Some(f),
|
||||
Self::RevealType => None,
|
||||
Self::RevealType | Self::Len => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2597,6 +2699,7 @@ impl KnownFunction {
|
||||
"issubclass" if definition.is_builtin_definition(db) => Some(
|
||||
KnownFunction::ConstraintFunction(KnownConstraintFunction::IsSubclass),
|
||||
),
|
||||
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -2650,27 +2753,6 @@ impl<'db> Class<'db> {
|
||||
.map(|ClassLiteralType { class }| class)
|
||||
}
|
||||
|
||||
/// Returns `true` if this class only contains fully-static entries in its MRO.
|
||||
fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
matches!(
|
||||
self.known(db),
|
||||
Some(
|
||||
// TODO: probably not complete/correct:
|
||||
KnownClass::Bool
|
||||
| KnownClass::Object
|
||||
| KnownClass::Bytes
|
||||
| KnownClass::Int
|
||||
| KnownClass::Float
|
||||
| KnownClass::Str
|
||||
| KnownClass::Set
|
||||
| KnownClass::List
|
||||
| KnownClass::Dict
|
||||
| KnownClass::Tuple
|
||||
| KnownClass::VersionInfo
|
||||
)
|
||||
) || self.iter_mro(db).all(ClassBase::is_fully_static)
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
let class_stmt = self.node(db);
|
||||
@@ -2727,7 +2809,11 @@ impl<'db> Class<'db> {
|
||||
pub fn is_subclass_of(self, db: &'db dyn Db, other: Class) -> bool {
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate, so we should not return `True` if we find `Any/Unknown` in the MRO.
|
||||
self.iter_mro(db).contains(&ClassBase::Class(other))
|
||||
self.is_subclass_of_base(db, other)
|
||||
}
|
||||
|
||||
fn is_subclass_of_base(self, db: &'db dyn Db, other: impl Into<ClassBase<'db>>) -> bool {
|
||||
self.iter_mro(db).contains(&other.into())
|
||||
}
|
||||
|
||||
/// Return the explicit `metaclass` of this class, if one is defined.
|
||||
@@ -3068,8 +3154,9 @@ pub struct StringLiteralType<'db> {
|
||||
}
|
||||
|
||||
impl<'db> StringLiteralType<'db> {
|
||||
pub fn len(&self, db: &'db dyn Db) -> usize {
|
||||
self.value(db).len()
|
||||
/// The length of the string, as would be returned by Python's `len()`.
|
||||
pub fn python_len(&self, db: &'db dyn Db) -> usize {
|
||||
self.value(db).chars().count()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3079,6 +3166,12 @@ pub struct BytesLiteralType<'db> {
|
||||
value: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl<'db> BytesLiteralType<'db> {
|
||||
pub fn python_len(&self, db: &'db dyn Db) -> usize {
|
||||
self.value(db).len()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
pub struct SliceLiteralType<'db> {
|
||||
start: Option<i32>,
|
||||
@@ -3132,38 +3225,16 @@ static_assertions::assert_eq_size!(Type, [u8; 16]);
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::db::tests::{setup_db, TestDb, TestDbBuilder};
|
||||
use crate::stdlib::typing_symbol;
|
||||
use crate::ProgramSettings;
|
||||
use crate::PythonVersion;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast as ast;
|
||||
use test_case::test_case;
|
||||
|
||||
pub(crate) fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
/// A test representation of a type that can be transformed unambiguously into a real Type,
|
||||
/// given a db.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -3634,13 +3705,28 @@ pub(crate) mod tests {
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::BooleanLiteral(false))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::NoDefaultType))]
|
||||
fn is_singleton(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(from.into_type(&db).is_singleton(&db));
|
||||
}
|
||||
|
||||
/// Explicitly test for Python version <3.13 and >=3.13, to ensure that
|
||||
/// the fallback to `typing_extensions` is working correctly.
|
||||
/// See [`KnownClass::canonical_module`] for more information.
|
||||
#[test_case(PythonVersion::PY312)]
|
||||
#[test_case(PythonVersion::PY313)]
|
||||
fn no_default_type_is_singleton(python_version: PythonVersion) {
|
||||
let db = TestDbBuilder::new()
|
||||
.with_python_version(python_version)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let no_default = Ty::KnownClassInstance(KnownClass::NoDefaultType).into_type(&db);
|
||||
|
||||
assert!(no_default.is_singleton(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::IntLiteral(1))]
|
||||
@@ -3686,17 +3772,9 @@ pub(crate) mod tests {
|
||||
#[test_case(Ty::StringLiteral("abc"))]
|
||||
#[test_case(Ty::LiteralString)]
|
||||
#[test_case(Ty::BytesLiteral("abc"))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Bool))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Object))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Bytes))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Type))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Int))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Float))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Str))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::List))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Tuple))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Set))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Dict))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Object))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Type))]
|
||||
#[test_case(Ty::BuiltinClassLiteral("str"))]
|
||||
#[test_case(Ty::TypingLiteral)]
|
||||
#[test_case(Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Str), Ty::None]))]
|
||||
@@ -3722,47 +3800,6 @@ pub(crate) mod tests {
|
||||
assert!(!from.into_type(&db).is_fully_static(&db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_fully_static_for_classes() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
# TODO: change this to `from typing import Any` once we understand that
|
||||
from unknown_module import UnknownClass
|
||||
|
||||
class FullyStaticBase: ...
|
||||
class FullyStatic(FullyStaticBase): ...
|
||||
|
||||
fully_static_instance = FullyStatic()
|
||||
|
||||
class GradualBase(UnknownClass): ...
|
||||
class Gradual(GradualBase): ...
|
||||
|
||||
gradual_instance = Gradual()
|
||||
"#,
|
||||
)?;
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py")?;
|
||||
|
||||
let fully_static_class = super::global_symbol(&db, module, "FullyStatic").expect_type();
|
||||
assert!(fully_static_class.is_class_literal());
|
||||
assert!(fully_static_class.is_fully_static(&db));
|
||||
|
||||
let fully_static_instance =
|
||||
super::global_symbol(&db, module, "fully_static_instance").expect_type();
|
||||
assert!(matches!(fully_static_instance, Type::Instance(_)));
|
||||
assert!(fully_static_instance.is_fully_static(&db));
|
||||
|
||||
let gradual_class = super::global_symbol(&db, module, "Gradual").expect_type();
|
||||
assert!(!gradual_class.is_fully_static(&db));
|
||||
|
||||
let gradual_instance = super::global_symbol(&db, module, "gradual_instance").expect_type();
|
||||
assert!(matches!(gradual_instance, Type::Instance(_)));
|
||||
assert!(!gradual_instance.is_fully_static(&db));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")]
|
||||
#[test_case(Ty::IntLiteral(-1))]
|
||||
#[test_case(Ty::StringLiteral("foo"))]
|
||||
@@ -3817,7 +3854,10 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn typing_vs_typeshed_no_default() {
|
||||
let db = setup_db();
|
||||
let db = TestDbBuilder::new()
|
||||
.with_python_version(PythonVersion::PY313)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let typing_no_default = typing_symbol(&db, "NoDefault").expect_type();
|
||||
let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault").expect_type();
|
||||
|
||||
@@ -378,35 +378,14 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use test_case::test_case;
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union() {
|
||||
let db = setup_db();
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -357,31 +357,10 @@ impl Display for DisplayStringLiteralType<'_> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{global_symbol, SliceLiteralType, StringLiteralType, Type, UnionType};
|
||||
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -379,6 +370,7 @@ impl<'db> ClassBase<'db> {
|
||||
| KnownInstanceType::NoReturn
|
||||
| KnownInstanceType::Never
|
||||
| KnownInstanceType::Optional => None,
|
||||
KnownInstanceType::Any => Some(Self::Any),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -390,14 +382,6 @@ impl<'db> ClassBase<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `false` if the base is a non-fully-static type like `Any` or `Unknown`.
|
||||
pub(crate) fn is_fully_static(self) -> bool {
|
||||
match self {
|
||||
Self::Class(_) => true,
|
||||
Self::Any | Self::Unknown | Self::Todo => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the MRO of this base
|
||||
fn mro(
|
||||
self,
|
||||
@@ -414,6 +398,12 @@ impl<'db> ClassBase<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Class<'db>> for ClassBase<'db> {
|
||||
fn from(value: Class<'db>) -> Self {
|
||||
ClassBase::Class(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: ClassBase<'db>) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
|
||||
use super::tests::{setup_db, Ty};
|
||||
use crate::db::tests::TestDb;
|
||||
use super::tests::Ty;
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::KnownClass;
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
|
||||
|
||||
@@ -189,32 +189,9 @@ impl<'db> Parameter<'db> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, FunctionType};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
pub(crate) fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
|
||||
#[track_caller]
|
||||
fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> {
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,8 @@ impl<'db> Unpacker<'db> {
|
||||
// there would be a cost and it's not clear that it's worth it.
|
||||
let value_ty = Type::tuple(
|
||||
self.db,
|
||||
std::iter::repeat(Type::LiteralString).take(string_literal_ty.len(self.db)),
|
||||
std::iter::repeat(Type::LiteralString)
|
||||
.take(string_literal_ty.python_len(self.db)),
|
||||
);
|
||||
self.unpack(target, value_ty, scope);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
50
crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py
vendored
Normal file
50
crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import math
|
||||
|
||||
|
||||
### Safely fixable
|
||||
|
||||
# Arguments are not checked
|
||||
int(id())
|
||||
int(len([]))
|
||||
int(ord(foo))
|
||||
int(hash(foo, bar))
|
||||
int(int(''))
|
||||
|
||||
int(math.comb())
|
||||
int(math.factorial())
|
||||
int(math.gcd())
|
||||
int(math.lcm())
|
||||
int(math.isqrt())
|
||||
int(math.perm())
|
||||
|
||||
|
||||
### Unsafe
|
||||
|
||||
int(math.ceil())
|
||||
int(math.floor())
|
||||
int(math.trunc())
|
||||
|
||||
|
||||
### `round()`
|
||||
|
||||
## Errors
|
||||
int(round(0))
|
||||
int(round(0, 0))
|
||||
int(round(0, None))
|
||||
|
||||
int(round(0.1))
|
||||
int(round(0.1, None))
|
||||
|
||||
# Argument type is not checked
|
||||
foo = type("Foo", (), {"__round__": lambda self: 4.2})()
|
||||
|
||||
int(round(foo))
|
||||
int(round(foo, 0))
|
||||
int(round(foo, None))
|
||||
|
||||
## No errors
|
||||
int(round(0, 3.14))
|
||||
int(round(0, non_literal))
|
||||
int(round(0, 0), base)
|
||||
int(round(0, 0, extra=keyword))
|
||||
int(round(0.1, 0))
|
||||
@@ -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)
|
||||
|
||||
@@ -1093,6 +1093,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::Airflow3Removal) {
|
||||
airflow::rules::removed_in_3(checker, expr);
|
||||
}
|
||||
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),
|
||||
@@ -983,6 +984,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern),
|
||||
(Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
|
||||
(Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral),
|
||||
(Ruff, "046") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryCastToInt),
|
||||
(Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing),
|
||||
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
|
||||
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user