Compare commits
50 Commits
0.11.2
...
dcreager/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eff2734bb | ||
|
|
640d821108 | ||
|
|
43ca85a351 | ||
|
|
338fed98a4 | ||
|
|
d70a3e6753 | ||
|
|
5697d21fca | ||
|
|
58350ec93b | ||
|
|
aae4d0f3eb | ||
|
|
807fce8069 | ||
|
|
8d16a5c8c9 | ||
|
|
4975c2f027 | ||
|
|
dd5b02aaa2 | ||
|
|
68ea2b8b5b | ||
|
|
e87fee4b3b | ||
|
|
cba197e3c5 | ||
|
|
66d0cf2a72 | ||
|
|
85b7f808e1 | ||
|
|
3a97bdf689 | ||
|
|
1bee3994aa | ||
|
|
888a910925 | ||
|
|
581b7005dc | ||
|
|
b442ba440f | ||
|
|
5aba72cdbd | ||
|
|
2711e08eb8 | ||
|
|
f5cdf23545 | ||
|
|
d98222cd14 | ||
|
|
f7b9089cb8 | ||
|
|
dfebc1cfe4 | ||
|
|
7e1484a9b1 | ||
|
|
187cac56bd | ||
|
|
890f79c4ab | ||
|
|
3899f7156f | ||
|
|
902d86e79e | ||
|
|
9fe89ddfba | ||
|
|
08a0995108 | ||
|
|
2d892bc9f7 | ||
|
|
ee51c2a389 | ||
|
|
bb07ccd783 | ||
|
|
c35f2bfe32 | ||
|
|
7fb765d9b6 | ||
|
|
0360c6b219 | ||
|
|
1cffb323bc | ||
|
|
92028efe3d | ||
|
|
7b86f54c4c | ||
|
|
e4f5fe8cf7 | ||
|
|
2baaedda6c | ||
|
|
b1deab83d9 | ||
|
|
d21d639ee0 | ||
|
|
14eb4cac88 | ||
|
|
c03c28d199 |
46
.github/workflows/build-binaries.yml
vendored
46
.github/workflows/build-binaries.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build sdist"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload sdist"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: wheels-sdist
|
||||
path: dist
|
||||
@@ -79,12 +79,12 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - x86_64"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: wheels-macos-x86_64
|
||||
path: dist
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-macos-x86_64
|
||||
path: |
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - aarch64"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: aarch64
|
||||
args: --release --locked --out dist
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: wheels-aarch64-apple-darwin
|
||||
path: dist
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-aarch64-apple-darwin
|
||||
path: |
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: --release --locked --out dist
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: wheels-${{ matrix.platform.target }}
|
||||
path: dist
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
|
||||
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-${{ matrix.platform.target }}
|
||||
path: |
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: wheels-${{ matrix.target }}
|
||||
path: dist
|
||||
@@ -260,7 +260,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-${{ matrix.target }}
|
||||
path: |
|
||||
@@ -304,7 +304,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: wheels-${{ matrix.platform.target }}
|
||||
path: dist
|
||||
@@ -343,7 +343,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-${{ matrix.platform.target }}
|
||||
path: |
|
||||
@@ -370,7 +370,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
@@ -387,7 +387,7 @@ jobs:
|
||||
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
.venv/bin/${{ env.MODULE_NAME }} --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: wheels-${{ matrix.target }}
|
||||
path: dist
|
||||
@@ -405,7 +405,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-${{ matrix.target }}
|
||||
path: |
|
||||
@@ -435,7 +435,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
.venv/bin/${{ env.MODULE_NAME }} --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: wheels-${{ matrix.platform.target }}
|
||||
path: dist
|
||||
@@ -472,7 +472,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-${{ matrix.platform.target }}
|
||||
path: |
|
||||
|
||||
6
.github/workflows/build-docker.yml
vendored
6
.github/workflows/build-docker.yml
vendored
@@ -96,7 +96,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_TUPLE }}
|
||||
path: /tmp/digests/*
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
|
||||
66
.github/workflows/ci.yaml
vendored
66
.github/workflows/ci.yaml
vendored
@@ -188,7 +188,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: |
|
||||
rustup component add clippy
|
||||
@@ -208,17 +208,17 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -239,7 +239,7 @@ jobs:
|
||||
env:
|
||||
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug/ruff
|
||||
@@ -254,17 +254,17 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -283,11 +283,11 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -310,7 +310,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -362,7 +362,7 @@ jobs:
|
||||
with:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
@@ -370,11 +370,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -394,7 +394,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -422,8 +422,8 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
|
||||
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
- uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
with:
|
||||
@@ -456,7 +456,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
@@ -492,7 +492,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
name: Download comparison Ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
@@ -587,13 +587,13 @@ jobs:
|
||||
run: |
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
name: Upload PR Number
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
name: Upload Results
|
||||
with:
|
||||
name: ecosystem-result
|
||||
@@ -625,11 +625,11 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
@@ -651,13 +651,13 @@ jobs:
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install pre-commit"
|
||||
run: pip install pre-commit
|
||||
- name: "Cache pre-commit"
|
||||
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
@@ -685,7 +685,7 @@ jobs:
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
|
||||
@@ -694,7 +694,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -724,7 +724,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Run checks"
|
||||
@@ -757,7 +757,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
name: Download development ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
@@ -793,7 +793,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -824,13 +824,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
|
||||
4
.github/workflows/daily_fuzz.yaml
vendored
4
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,12 +34,12 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
|
||||
- uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: Build ruff
|
||||
# A debug build means the script runs slower once it gets started,
|
||||
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
|
||||
|
||||
2
.github/workflows/daily_property_tests.yaml
vendored
2
.github/workflows/daily_property_tests.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: Build Red Knot
|
||||
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
|
||||
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.
|
||||
|
||||
8
.github/workflows/mypy_primer.yaml
vendored
8
.github/workflows/mypy_primer.yaml
vendored
@@ -35,9 +35,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
- name: Install Rust toolchain
|
||||
@@ -81,13 +81,13 @@ jobs:
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- name: Upload diff
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: mypy_primer_diff
|
||||
path: mypy_primer.diff
|
||||
|
||||
- name: Upload pr-number
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
2
.github/workflows/publish-docs.yml
vendored
2
.github/workflows/publish-docs.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
|
||||
@@ -8,10 +8,10 @@ on:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "crates/red_knot*/**"
|
||||
- "crates/ruff_db"
|
||||
- "crates/ruff_python_ast"
|
||||
- "crates/ruff_python_parser"
|
||||
- "playground"
|
||||
- "crates/ruff_db/**"
|
||||
- "crates/ruff_python_ast/**"
|
||||
- "crates/ruff_python_parser/**"
|
||||
- "playground/**"
|
||||
- ".github/workflows/publish-knot-playground.yml"
|
||||
|
||||
concurrency:
|
||||
|
||||
4
.github/workflows/publish-pypi.yml
vendored
4
.github/workflows/publish-pypi.yml
vendored
@@ -22,8 +22,8 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
|
||||
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
pattern: wheels-*
|
||||
path: wheels
|
||||
|
||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
@@ -125,14 +125,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
@@ -175,14 +175,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
@@ -251,7 +251,7 @@ jobs:
|
||||
submodules: recursive
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1421,11 +1421,11 @@ The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`cancel-scope-no-checkpoint`](https://docs.astral.sh/ruff/rules/cancel-scope-no-checkpoint/) (`ASYNC100`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-function-with-timeout`](https://docs.astral.sh/ruff/rules/async-function-with-timeout/) (`ASYNC109`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-busy-wait`](https://docs.astral.sh/ruff/rules/async-busy-wait/) (`ASYNC110`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-zero-sleep`](https://docs.astral.sh/ruff/rules/async-zero-sleep/) (`ASYNC115`): Support `anyio` context mangers.
|
||||
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever/) (`ASYNC116`): Support `anyio` context mangers.
|
||||
- [`cancel-scope-no-checkpoint`](https://docs.astral.sh/ruff/rules/cancel-scope-no-checkpoint/) (`ASYNC100`): Support `asyncio` and `anyio` context managers.
|
||||
- [`async-function-with-timeout`](https://docs.astral.sh/ruff/rules/async-function-with-timeout/) (`ASYNC109`): Support `asyncio` and `anyio` context managers.
|
||||
- [`async-busy-wait`](https://docs.astral.sh/ruff/rules/async-busy-wait/) (`ASYNC110`): Support `asyncio` and `anyio` context managers.
|
||||
- [`async-zero-sleep`](https://docs.astral.sh/ruff/rules/async-zero-sleep/) (`ASYNC115`): Support `anyio` context managers.
|
||||
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever/) (`ASYNC116`): Support `anyio` context managers.
|
||||
|
||||
The following fixes have been stabilized:
|
||||
|
||||
|
||||
63
Cargo.lock
generated
63
Cargo.lock
generated
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -487,7 +487,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -918,7 +918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1071,16 +1071,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.13.3+wasi-0.2.2",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1499,7 +1499,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1659,9 +1659,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
|
||||
checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1797,9 +1797,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.43"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
|
||||
checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -2384,6 +2384,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -2441,7 +2447,7 @@ version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"getrandom 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2637,15 +2643,16 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"getrandom 0.3.1",
|
||||
"getrandom 0.3.2",
|
||||
"js-sys",
|
||||
"log",
|
||||
"red_knot_project",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
@@ -3289,7 +3296,7 @@ version = "0.11.2"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"getrandom 0.3.1",
|
||||
"getrandom 0.3.2",
|
||||
"js-sys",
|
||||
"log",
|
||||
"ruff_formatter",
|
||||
@@ -3379,7 +3386,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3392,7 +3399,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.3",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3769,15 +3776,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.19.0"
|
||||
version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600"
|
||||
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.1",
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix 1.0.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4270,7 +4277,7 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"getrandom 0.3.2",
|
||||
"js-sys",
|
||||
"rand 0.9.0",
|
||||
"uuid-macro-internal",
|
||||
@@ -4378,9 +4385,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.13.3+wasi-0.2.2"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
@@ -4543,7 +4550,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4796,9 +4803,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.33.0"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
]
|
||||
|
||||
@@ -12,7 +12,7 @@ use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform};
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{
|
||||
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
|
||||
file_time_now, OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
|
||||
};
|
||||
use ruff_db::{Db as _, Upcast};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
@@ -462,7 +462,7 @@ fn update_file(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()
|
||||
|
||||
std::thread::sleep(Duration::from_nanos(10));
|
||||
|
||||
filetime::set_file_handle_times(&file, None, Some(filetime::FileTime::now()))?;
|
||||
filetime::set_file_handle_times(&file, None, Some(file_time_now()))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ impl ProjectMetadata {
|
||||
}
|
||||
|
||||
/// Loads a project from a set of options with an optional pyproject-project table.
|
||||
pub(crate) fn from_options(
|
||||
pub fn from_options(
|
||||
mut options: Options,
|
||||
root: SystemPathBuf,
|
||||
project: Option<&Project>,
|
||||
|
||||
@@ -37,11 +37,19 @@ pub struct Options {
|
||||
|
||||
impl Options {
|
||||
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, KnotTomlError> {
|
||||
let _guard = ValueSourceGuard::new(source);
|
||||
let _guard = ValueSourceGuard::new(source, true);
|
||||
let options = toml::from_str(content)?;
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
pub fn deserialize_with<'de, D>(source: ValueSource, deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let _guard = ValueSourceGuard::new(source, false);
|
||||
Self::deserialize(deserializer)
|
||||
}
|
||||
|
||||
pub(crate) fn to_program_settings(
|
||||
&self,
|
||||
project_root: &SystemPath,
|
||||
|
||||
@@ -34,7 +34,7 @@ impl PyProject {
|
||||
content: &str,
|
||||
source: ValueSource,
|
||||
) -> Result<Self, PyProjectError> {
|
||||
let _guard = ValueSourceGuard::new(source);
|
||||
let _guard = ValueSourceGuard::new(source, true);
|
||||
toml::from_str(content).map_err(PyProjectError::TomlSyntax)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ pub enum ValueSource {
|
||||
/// Ideally, we'd use [`ruff_db::files::File`] but we can't because the database hasn't been
|
||||
/// created when loading the configuration.
|
||||
File(Arc<SystemPathBuf>),
|
||||
|
||||
/// The value comes from a CLI argument, while it's left open if specified using a short argument,
|
||||
/// long argument (`--extra-paths`) or `--config key=value`.
|
||||
Cli,
|
||||
@@ -41,18 +42,18 @@ thread_local! {
|
||||
/// Use the [`ValueSourceGuard`] to initialize the thread local before calling into any
|
||||
/// deserialization code. It ensures that the thread local variable gets cleaned up
|
||||
/// once deserialization is done (once the guard gets dropped).
|
||||
static VALUE_SOURCE: RefCell<Option<ValueSource>> = const { RefCell::new(None) };
|
||||
static VALUE_SOURCE: RefCell<Option<(ValueSource, bool)>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Guard to safely change the [`VALUE_SOURCE`] for the current thread.
|
||||
#[must_use]
|
||||
pub(super) struct ValueSourceGuard {
|
||||
prev_value: Option<ValueSource>,
|
||||
prev_value: Option<(ValueSource, bool)>,
|
||||
}
|
||||
|
||||
impl ValueSourceGuard {
|
||||
pub(super) fn new(source: ValueSource) -> Self {
|
||||
let prev = VALUE_SOURCE.replace(Some(source));
|
||||
pub(super) fn new(source: ValueSource, is_toml: bool) -> Self {
|
||||
let prev = VALUE_SOURCE.replace(Some((source, is_toml)));
|
||||
Self { prev_value: prev }
|
||||
}
|
||||
}
|
||||
@@ -265,18 +266,24 @@ where
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
|
||||
let span = spanned.span();
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(span.start).expect("Configuration file to be smaller than 4GB"),
|
||||
TextSize::try_from(span.end).expect("Configuration file to be smaller than 4GB"),
|
||||
);
|
||||
VALUE_SOURCE.with_borrow(|source| {
|
||||
let (source, has_span) = source.clone().unwrap();
|
||||
|
||||
Ok(VALUE_SOURCE.with_borrow(|source| {
|
||||
let source = source.clone().unwrap();
|
||||
if has_span {
|
||||
let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
|
||||
let span = spanned.span();
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(span.start)
|
||||
.expect("Configuration file to be smaller than 4GB"),
|
||||
TextSize::try_from(span.end)
|
||||
.expect("Configuration file to be smaller than 4GB"),
|
||||
);
|
||||
|
||||
Self::with_range(spanned.into_inner(), source, range)
|
||||
}))
|
||||
Ok(Self::with_range(spanned.into_inner(), source, range))
|
||||
} else {
|
||||
Ok(Self::new(T::deserialize(deserializer)?, source))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ References:
|
||||
|
||||
- <https://typing.readthedocs.io/en/latest/spec/callables.html#callable>
|
||||
|
||||
TODO: Use `collections.abc` as importing from `typing` is deprecated but this requires support for
|
||||
`*` imports. See: <https://docs.python.org/3/library/typing.html#deprecated-aliases>.
|
||||
Note that `typing.Callable` is deprecated at runtime, in favour of `collections.abc.Callable` (see:
|
||||
<https://docs.python.org/3/library/typing.html#deprecated-aliases>). However, removal of
|
||||
`typing.Callable` is not currently planned, and the canonical location of the stub for the symbol in
|
||||
typeshed is still `typing.pyi`.
|
||||
|
||||
## Invalid forms
|
||||
|
||||
@@ -152,6 +154,39 @@ def _(c: Callable[[int, str], int]):
|
||||
reveal_type(c) # revealed: (int, str, /) -> int
|
||||
```
|
||||
|
||||
## Union
|
||||
|
||||
```py
|
||||
from typing import Callable, Union
|
||||
|
||||
def _(
|
||||
c: Callable[[Union[int, str]], int] | None,
|
||||
d: None | Callable[[Union[int, str]], int],
|
||||
e: None | Callable[[Union[int, str]], int] | int,
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) | None
|
||||
reveal_type(d) # revealed: None | ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: None | ((int | str, /) -> int) | int
|
||||
```
|
||||
|
||||
## Intersection
|
||||
|
||||
```py
|
||||
from typing import Callable, Union
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
def _(
|
||||
c: Intersection[Callable[[Union[int, str]], int], int],
|
||||
d: Intersection[int, Callable[[Union[int, str]], int]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], str],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, str]]]],
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) & int
|
||||
reveal_type(d) # revealed: int & ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & str
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & str)
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
A nested `Callable` as one of the parameter types:
|
||||
|
||||
@@ -81,8 +81,7 @@ reveal_type(DictSubclass.__mro__)
|
||||
|
||||
class SetSubclass(typing.Set): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Unknown, Literal[object]]
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
||||
reveal_type(SetSubclass.__mro__)
|
||||
|
||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||
@@ -114,8 +113,7 @@ reveal_type(DefaultDictSubclass.__mro__)
|
||||
|
||||
class DequeSubclass(typing.Deque): ...
|
||||
|
||||
# TODO: Should be (DequeSubclass, deque, MutableSequence, Sequence, Reversible, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Unknown, Literal[object]]
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
||||
reveal_type(DequeSubclass.__mro__)
|
||||
|
||||
class OrderedDictSubclass(typing.OrderedDict): ...
|
||||
|
||||
@@ -29,8 +29,6 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
|
||||
# TODO: not an error; remove once `call` is implemented for `Callable`
|
||||
# error: [call-non-callable]
|
||||
return callback(42, *args, **kwargs)
|
||||
|
||||
class Foo:
|
||||
|
||||
@@ -551,6 +551,7 @@ reveal_type(C().x) # revealed: str
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
self.x: int = len(1, 2, 3)
|
||||
```
|
||||
|
||||
@@ -697,10 +698,10 @@ class Base:
|
||||
self.defined_in_init: str | None = "value in base"
|
||||
|
||||
class Intermediate(Base):
|
||||
# Re-declaring base class attributes with the *same *type is fine:
|
||||
# Redeclaring base class attributes with the *same *type is fine:
|
||||
base_class_attribute_1: str | None = None
|
||||
|
||||
# Re-declaring them with a *narrower type* is unsound, because modifications
|
||||
# Redeclaring them with a *narrower type* is unsound, because modifications
|
||||
# through a `Base` reference could violate that constraint.
|
||||
#
|
||||
# Mypy does not report an error here, but pyright does: "… overrides symbol
|
||||
@@ -712,7 +713,7 @@ class Intermediate(Base):
|
||||
# TODO: This should be an error
|
||||
base_class_attribute_2: str
|
||||
|
||||
# Re-declaring attributes with a *wider type* directly violates LSP.
|
||||
# Redeclaring attributes with a *wider type* directly violates LSP.
|
||||
#
|
||||
# In this case, both mypy and pyright report an error.
|
||||
#
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# `typing.Callable`
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[], int]):
|
||||
reveal_type(c()) # revealed: int
|
||||
|
||||
def _(c: Callable[[int, str], int]):
|
||||
reveal_type(c(1, "a")) # revealed: int
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal["a"]` cannot be assigned to parameter 1; expected type `int`"
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2; expected type `str`"
|
||||
reveal_type(c("a", 1)) # revealed: int
|
||||
```
|
||||
|
||||
The `Callable` annotation can only be used to describe positional-only parameters.
|
||||
|
||||
```py
|
||||
def _(c: Callable[[int, str], None]):
|
||||
# error: [unknown-argument] "Argument `a` does not match any known parameter"
|
||||
# error: [unknown-argument] "Argument `b` does not match any known parameter"
|
||||
# error: [missing-argument] "No arguments provided for required parameters 1, 2"
|
||||
reveal_type(c(a=1, b="b")) # revealed: None
|
||||
```
|
||||
|
||||
If the annotation uses a gradual form (`...`) for the parameter list, then it can accept any kind of
|
||||
parameter with any type.
|
||||
|
||||
```py
|
||||
def _(c: Callable[..., int]):
|
||||
reveal_type(c()) # revealed: int
|
||||
reveal_type(c(1)) # revealed: int
|
||||
reveal_type(c(1, "str", False, a=[1, 2], b=(3, 4))) # revealed: int
|
||||
```
|
||||
|
||||
An invalid `Callable` form can accept any parameters and will return `Unknown`.
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form]
|
||||
def _(c: Callable[42, str]):
|
||||
reveal_type(c()) # revealed: Unknown
|
||||
```
|
||||
@@ -37,8 +37,6 @@ def foo() -> int:
|
||||
return 42
|
||||
|
||||
def decorator(func) -> Callable[[], int]:
|
||||
# TODO: no error
|
||||
# error: [invalid-return-type]
|
||||
return foo
|
||||
|
||||
@decorator
|
||||
|
||||
@@ -161,3 +161,17 @@ def _(flag: bool):
|
||||
reveal_type(repr("string")) # revealed: Literal["'string'"]
|
||||
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
|
||||
```
|
||||
|
||||
## Cannot use an argument as both a value and a type form
|
||||
|
||||
```py
|
||||
from knot_extensions import is_fully_static
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = repr
|
||||
else:
|
||||
f = is_fully_static
|
||||
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
|
||||
reveal_type(f(int)) # revealed: str | Literal[True]
|
||||
```
|
||||
|
||||
@@ -13,17 +13,16 @@ reveal_type(cast("str", True)) # revealed: str
|
||||
|
||||
reveal_type(cast(int | str, 1)) # revealed: int | str
|
||||
|
||||
reveal_type(cast(val="foo", typ=int)) # revealed: int
|
||||
|
||||
# error: [invalid-type-form]
|
||||
reveal_type(cast(Literal, True)) # revealed: Unknown
|
||||
|
||||
# error: [invalid-type-form]
|
||||
reveal_type(cast(1, True)) # revealed: Unknown
|
||||
|
||||
# TODO: These should be errors
|
||||
# error: [missing-argument] "No argument provided for required parameter `val` of function `cast`"
|
||||
cast(str)
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
|
||||
cast(str, b"ar", "foo")
|
||||
|
||||
# TODO: Either support keyword arguments properly,
|
||||
# or give a comprehensible error message saying they're unsupported
|
||||
cast(val="foo", typ=int) # error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ try:
|
||||
help()
|
||||
except* OSError as e:
|
||||
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
|
||||
# (needs homogenous tuples + generics)
|
||||
# (needs homogeneous tuples + generics)
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ try:
|
||||
help()
|
||||
except* (TypeError, AttributeError) as e:
|
||||
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
|
||||
# (needs homogenous tuples + generics)
|
||||
# (needs homogeneous tuples + generics)
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Using a parameter with default value:
|
||||
lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
Using a variadic paramter:
|
||||
Using a variadic parameter:
|
||||
|
||||
```py
|
||||
# TODO: should be `tuple[Unknown, ...]` (needs generics)
|
||||
@@ -98,3 +98,22 @@ expression.
|
||||
```py
|
||||
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> Unknown) -> Unknown
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
This does not enumerate all combinations of parameter kinds as that should be covered by the
|
||||
[subtype tests for callable types](./../type_properties/is_subtype_of.md#callable).
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
a1: Callable[[], None] = lambda: None
|
||||
a2: Callable[[int], None] = lambda x: None
|
||||
a3: Callable[[int, int], None] = lambda x, y, z=1: None
|
||||
a4: Callable[[int, int], None] = lambda *args: None
|
||||
|
||||
# error: [invalid-assignment]
|
||||
a5: Callable[[], None] = lambda x: None
|
||||
# error: [invalid-assignment]
|
||||
a6: Callable[[int], None] = lambda: None
|
||||
```
|
||||
|
||||
@@ -229,6 +229,6 @@ reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
|
||||
```py
|
||||
class NoDunderLen: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(len(NoDunderLen())) # revealed: int
|
||||
```
|
||||
|
||||
@@ -57,12 +57,30 @@ def f() -> int:
|
||||
### In Protocol
|
||||
|
||||
```py
|
||||
from typing import Protocol
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
class Bar(Protocol):
|
||||
def f(self) -> int: ...
|
||||
|
||||
class Baz(Bar):
|
||||
# error: [invalid-return-type]
|
||||
def f(self) -> int: ...
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Qux(Protocol[T]):
|
||||
# TODO: no error
|
||||
# error: [invalid-return-type]
|
||||
def f(self) -> int: ...
|
||||
|
||||
class Foo(Protocol):
|
||||
def f[T](self, v: T) -> T: ...
|
||||
|
||||
t = (Protocol, int)
|
||||
reveal_type(t[0]) # revealed: typing.Protocol
|
||||
|
||||
class Lorem(t[0]):
|
||||
def f(self) -> int: ...
|
||||
```
|
||||
|
||||
### In abstract method
|
||||
@@ -72,12 +90,20 @@ from abc import ABC, abstractmethod
|
||||
|
||||
class Foo(ABC):
|
||||
@abstractmethod
|
||||
# TODO: no error
|
||||
# error: [invalid-return-type]
|
||||
def f(self) -> int: ...
|
||||
@abstractmethod
|
||||
# error: [invalid-return-type]
|
||||
def g[T](self, x: T) -> T: ...
|
||||
|
||||
class Bar[T](ABC):
|
||||
@abstractmethod
|
||||
def f(self) -> int: ...
|
||||
@abstractmethod
|
||||
def g[T](self, x: T) -> T: ...
|
||||
|
||||
# error: [invalid-return-type]
|
||||
def f() -> int: ...
|
||||
@abstractmethod # Semantically meaningless, accepted nevertheless
|
||||
def g() -> int: ...
|
||||
```
|
||||
|
||||
### In overload
|
||||
|
||||
@@ -183,8 +183,9 @@ In a non-stub file, without stringified forward references, this raises a `NameE
|
||||
```py
|
||||
class Base[T]: ...
|
||||
|
||||
# TODO: error: [unresolved-reference]
|
||||
# TODO: the unresolved-reference error is correct, the non-subscriptable is not
|
||||
# error: [non-subscriptable]
|
||||
# error: [unresolved-reference]
|
||||
class Sub(Base[Sub]): ...
|
||||
```
|
||||
|
||||
|
||||
951
crates/red_knot_python_semantic/resources/mdtest/import/star.md
Normal file
951
crates/red_knot_python_semantic/resources/mdtest/import/star.md
Normal file
@@ -0,0 +1,951 @@
|
||||
# Wildcard (`*`) imports
|
||||
|
||||
See the [Python language reference for import statements].
|
||||
|
||||
## Basic functionality
|
||||
|
||||
### A simple `*` import
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
print(Y) # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
### Overriding existing definition
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
X = 42
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
```
|
||||
|
||||
### Overridden by later definition
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
X = False
|
||||
reveal_type(X) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
### Reaching across many modules
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from b import *
|
||||
```
|
||||
|
||||
`d.py`:
|
||||
|
||||
```py
|
||||
from c import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
```
|
||||
|
||||
### A wildcard import constitutes a re-export
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
Y: bool = False
|
||||
```
|
||||
|
||||
`c.pyi`:
|
||||
|
||||
```pyi
|
||||
from a import *
|
||||
from b import Y
|
||||
```
|
||||
|
||||
`d.py`:
|
||||
|
||||
```py
|
||||
# `X` is accessible because the `*` import in `c` re-exports it from `c`
|
||||
from c import X
|
||||
|
||||
# but `Y` is not because the `from b import Y` import does *not* constitute a re-export
|
||||
from c import Y # error: [unresolved-import]
|
||||
```
|
||||
|
||||
### Global-scope symbols defined using walrus expressions
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X = (Y := 3) + 4
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: Unknown | Literal[7]
|
||||
reveal_type(Y) # revealed: Unknown | Literal[3]
|
||||
```
|
||||
|
||||
### Global-scope symbols defined in many other ways
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict as Foo
|
||||
|
||||
A, B = 1, (C := 2)
|
||||
D: (E := 4) = (F := 5) # error: [invalid-type-form]
|
||||
|
||||
for G in [1]:
|
||||
...
|
||||
|
||||
for (H := 4).whatever in [2]: # error: [unresolved-attribute]
|
||||
...
|
||||
|
||||
class I: ...
|
||||
|
||||
def J(): ...
|
||||
|
||||
type K = int
|
||||
|
||||
with () as L: # error: [invalid-context-manager]
|
||||
...
|
||||
|
||||
match 42:
|
||||
case {"something": M}:
|
||||
...
|
||||
case [*N]:
|
||||
...
|
||||
case [O]:
|
||||
...
|
||||
case P | Q:
|
||||
...
|
||||
case object(foo=R):
|
||||
...
|
||||
case object(S):
|
||||
...
|
||||
case T:
|
||||
...
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
# fmt: off
|
||||
|
||||
print((
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
E,
|
||||
F,
|
||||
G, # TODO: could emit diagnostic about being possibly unbound
|
||||
H,
|
||||
I,
|
||||
J,
|
||||
K,
|
||||
L,
|
||||
M, # TODO: could emit diagnostic about being possibly unbound
|
||||
N, # TODO: could emit diagnostic about being possibly unbound
|
||||
O, # TODO: could emit diagnostic about being possibly unbound
|
||||
P, # TODO: could emit diagnostic about being possibly unbound
|
||||
Q, # TODO: could emit diagnostic about being possibly unbound
|
||||
R, # TODO: could emit diagnostic about being possibly unbound
|
||||
S, # TODO: could emit diagnostic about being possibly unbound
|
||||
T, # TODO: could emit diagnostic about being possibly unbound
|
||||
typing,
|
||||
OrderedDict,
|
||||
Foo,
|
||||
))
|
||||
```
|
||||
|
||||
### Definitions in function-like scopes are not global definitions
|
||||
|
||||
Except for some cases involving walrus expressions inside comprehension scopes.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
[a for a in Iterable()]
|
||||
{b for b in Iterable()}
|
||||
{c: c for c in Iterable()}
|
||||
(d for d in Iterable())
|
||||
lambda e: (f := 42)
|
||||
|
||||
# Definitions created by walruses in a comprehension scope are unique;
|
||||
# they "leak out" of the scope and are stored in the surrounding scope
|
||||
[(g := h * 2) for h in Iterable()]
|
||||
[i for j in Iterable() if (i := j - 10) > 0]
|
||||
{(k := l * 2): (m := l * 3) for l in Iterable()}
|
||||
list(((o := p * 2) for p in Iterable()))
|
||||
|
||||
# A walrus expression nested inside several scopes *still* leaks out
|
||||
# to the global scope:
|
||||
[[[[(q := r) for r in Iterable()]] for _ in range(42)] for _ in range(42)]
|
||||
|
||||
# A walrus inside a lambda inside a comprehension does not leak out
|
||||
[(lambda s=s: (t := 42))() for s in Iterable()]
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(a) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(c) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(d) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(f) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(h) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(j) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(p) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(r) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(s) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(t) # revealed: Unknown
|
||||
|
||||
# TODO: these should all reveal `Unknown | int`.
|
||||
# (We don't generally model elsewhere in red-knot that bindings from walruses
|
||||
# "leak" from comprehension scopes into outer scopes, but we should.)
|
||||
# See https://github.com/astral-sh/ruff/issues/16954
|
||||
reveal_type(g) # revealed: Unknown
|
||||
reveal_type(i) # revealed: Unknown
|
||||
reveal_type(k) # revealed: Unknown
|
||||
reveal_type(m) # revealed: Unknown
|
||||
reveal_type(o) # revealed: Unknown
|
||||
reveal_type(q) # revealed: Unknown
|
||||
```
|
||||
|
||||
### An annotation without a value is a definition in a stub but not a `.py` file
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
X: bool
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
Y: bool
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from b import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Global-scope names starting with underscores
|
||||
|
||||
Global-scope names starting with underscores are not imported from a `*` import (unless the module
|
||||
has `__all__` and they are included in `__all__`):
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
_private: bool = False
|
||||
__protected: bool = False
|
||||
__dunder__: bool = False
|
||||
___thunder___: bool = False
|
||||
|
||||
Y: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(_private) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(__protected) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(__dunder__) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(___thunder___) # revealed: Unknown
|
||||
|
||||
reveal_type(Y) # revealed: bool
|
||||
```
|
||||
|
||||
### All public symbols are considered re-exported from `.py` files
|
||||
|
||||
For `.py` files, we should consider all public symbols in the global namespace exported by that
|
||||
module when considering which symbols are made available by a `*` import. Here, `b.py` does not use
|
||||
the explicit `from a import X as X` syntax to explicitly mark it as publicly re-exported, and `X` is
|
||||
not included in `__all__`; whether it should be considered a "public name" in module `b` is
|
||||
ambiguous. We could consider an opt-in rule to warn the user when they use `X` in `c.py` that it was
|
||||
not included in `__all__` and was not marked as an explicit re-export.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import X
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from b import *
|
||||
|
||||
# TODO: we could consider an opt-in diagnostic (see prose commentary above)
|
||||
reveal_type(X) # revealed: bool
|
||||
```
|
||||
|
||||
### Only explicit re-exports are considered re-exported from `.pyi` files
|
||||
|
||||
For `.pyi` files, we should consider all imports private to the stub unless they are included in
|
||||
`__all__` or use the explicit `from foo import X as X` syntax.
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
X: bool = True
|
||||
Y: bool = True
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
from a import X, Y as Y
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from b import *
|
||||
|
||||
# This error is correct, as `X` is not considered re-exported from module `b`:
|
||||
#
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
||||
reveal_type(Y) # revealed: bool
|
||||
```
|
||||
|
||||
### Symbols in statically known branches
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
X: bool = True
|
||||
else:
|
||||
Y: bool = False
|
||||
Z: int = 42
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
Z: bool = True
|
||||
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# TODO: should emit error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
|
||||
# TODO: The `*` import should not be considered a redefinition
|
||||
# of the global variable in this module, as the symbol in
|
||||
# the `a` module is in a branch that is statically known
|
||||
# to be dead code given the `python-version` configuration.
|
||||
# Thus this should reveal `Literal[True]`.
|
||||
reveal_type(Z) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Relative `*` imports
|
||||
|
||||
Relative `*` imports are also supported by Python:
|
||||
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`a/foo.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`a/bar.py`:
|
||||
|
||||
```py
|
||||
from .foo import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
```
|
||||
|
||||
## Star imports with `__all__`
|
||||
|
||||
If a module `x` contains `__all__`, all symbols included in `x.__all__` are imported by
|
||||
`from x import *` (but no other symbols are).
|
||||
|
||||
### Simple tuple `__all__`
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("X", "_private", "__protected", "__dunder__", "___thunder___")
|
||||
|
||||
X: bool = True
|
||||
_private: bool = True
|
||||
__protected: bool = True
|
||||
__dunder__: bool = True
|
||||
___thunder___: bool = True
|
||||
|
||||
Y: bool = False
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# TODO none of these should error, should all reveal `bool`
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(_private) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(__protected) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(__dunder__) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(___thunder___) # revealed: Unknown
|
||||
|
||||
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(Y) # revealed: bool
|
||||
```
|
||||
|
||||
### Simple list `__all__`
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["X"]
|
||||
|
||||
X: bool = True
|
||||
Y: bool = False
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(Y) # revealed: bool
|
||||
```
|
||||
|
||||
### `__all__` with additions later on in the global scope
|
||||
|
||||
The [typing spec] lists certain modifications to `__all__` that must be understood by type checkers.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
FOO: bool = True
|
||||
|
||||
__all__ = ["FOO"]
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
import a
|
||||
from a import *
|
||||
|
||||
__all__ = ["A"]
|
||||
__all__ += ["B"]
|
||||
__all__.append("C")
|
||||
__all__.extend(["D"])
|
||||
__all__.extend(("E",))
|
||||
__all__.extend(a.__all__)
|
||||
|
||||
A: bool = True
|
||||
B: bool = True
|
||||
C: bool = True
|
||||
D: bool = True
|
||||
E: bool = True
|
||||
F: bool = False
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from b import *
|
||||
|
||||
reveal_type(A) # revealed: bool
|
||||
reveal_type(B) # revealed: bool
|
||||
reveal_type(C) # revealed: bool
|
||||
reveal_type(D) # revealed: bool
|
||||
reveal_type(E) # revealed: bool
|
||||
reveal_type(FOO) # revealed: bool
|
||||
|
||||
# TODO should error with [unresolved-reference] & reveal `Unknown`
|
||||
reveal_type(F) # revealed: bool
|
||||
```
|
||||
|
||||
### `__all__` with subtractions later on in the global scope
|
||||
|
||||
Whereas there are many ways of adding to `__all__` that type checkers must support, there is only
|
||||
one way of subtracting from `__all__` that type checkers are required to support:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
__all__.remove("A")
|
||||
|
||||
A: bool = True
|
||||
B: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(A) # revealed: bool
|
||||
|
||||
# TODO should emit an [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(B) # revealed: bool
|
||||
```
|
||||
|
||||
### Invalid `__all__`
|
||||
|
||||
If `a.__all__` contains a member that does not refer to a symbol with bindings in the global scope,
|
||||
a wildcard import from module `a` will fail at runtime.
|
||||
|
||||
TODO: Should we:
|
||||
|
||||
1. Emit a diagnostic at the invalid definition of `__all__` (which will not fail at runtime)?
|
||||
1. Emit a diagnostic at the star-import from the module with the invalid `__all__` (which _will_
|
||||
fail at runtime)?
|
||||
1. Emit a diagnostic on both?
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["a", "b"]
|
||||
|
||||
a = 42
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
# TODO we should consider emitting a diagnostic here (see prose description above)
|
||||
from a import * # fails with `AttributeError: module 'foo' has no attribute 'b'` at runtime
|
||||
```
|
||||
|
||||
### Dynamic `__all__`
|
||||
|
||||
If `__all__` contains members that are dynamically computed, we should check that all members of
|
||||
`__all__` are assignable to `str`. For the purposes of evaluating `*` imports, however, we should
|
||||
treat the module as though it has no `__all__` at all: all global-scope members of the module should
|
||||
be considered imported by the import statement. We should probably also emit a warning telling the
|
||||
user that we cannot statically determine the elements of `__all__`.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f() -> str:
|
||||
return "f"
|
||||
|
||||
def g() -> int:
|
||||
return 42
|
||||
|
||||
# TODO we should emit a warning here for the dynamically constructed `__all__` member.
|
||||
__all__ = [f()]
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
# At runtime, `f` is imported but `g` is not; to avoid false positives, however,
|
||||
# we treat `a` as though it does not have `__all__` at all,
|
||||
# which would imply that both symbols would be present.
|
||||
reveal_type(f) # revealed: Literal[f]
|
||||
reveal_type(g) # revealed: Literal[g]
|
||||
```
|
||||
|
||||
### `__all__` conditionally defined in a statically known branch
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
X: bool = True
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
__all__ = ["X", "Y"]
|
||||
Y: bool = True
|
||||
else:
|
||||
__all__ = ("Z",)
|
||||
Z: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
reveal_type(Y) # revealed: bool
|
||||
|
||||
# TODO: should error with [unresolved-reference]
|
||||
reveal_type(Z) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `__all__` conditionally mutated in a statically known branch
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["X"]
|
||||
X: bool = True
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
__all__.append("Y")
|
||||
Y: bool = True
|
||||
else:
|
||||
__all__.append("Z")
|
||||
Z: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
reveal_type(Y) # revealed: bool
|
||||
|
||||
# TODO should have an [unresolved-reference] diagnostic
|
||||
reveal_type(Z) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Empty `__all__`
|
||||
|
||||
An empty `__all__` is valid, but a `*` import from a module with an empty `__all__` results in 0
|
||||
bindings being added from the import:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
|
||||
__all__ = ()
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
Y: bool = True
|
||||
|
||||
__all__ = []
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from b import *
|
||||
|
||||
# TODO: both of these should have [unresolved-reference] diagnostics and reveal `Unknown`
|
||||
reveal_type(X) # revealed: bool
|
||||
reveal_type(Y) # revealed: bool
|
||||
```
|
||||
|
||||
### `__all__` in a stub file
|
||||
|
||||
If a name is included in `__all__` in a stub file, it is considered re-exported even if it was only
|
||||
defined using an import without the explicit `from foo import X as X` syntax:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
Y: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import X, Y
|
||||
|
||||
__all__ = ["X"]
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from b import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# TODO this should have an [unresolved-reference] diagnostic and reveal `Unknown`
|
||||
reveal_type(Y) # revealed: bool
|
||||
```
|
||||
|
||||
## `global` statements in non-global scopes
|
||||
|
||||
A `global` statement in a nested function scope, combined with a definition in the same function
|
||||
scope of the name that was declared `global`, can add a symbol to the global namespace.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f():
|
||||
global g, h
|
||||
|
||||
g: bool = True
|
||||
|
||||
f()
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(f) # revealed: Literal[f]
|
||||
|
||||
# TODO: false positive, should be `bool` with no diagnostic
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(g) # revealed: Unknown
|
||||
|
||||
# this diagnostic is accurate, though!
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(h) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Cyclic star imports
|
||||
|
||||
Believe it or not, this code does _not_ raise an exception at runtime!
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
from b import *
|
||||
|
||||
A: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
B: bool = True
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
reveal_type(A) # revealed: bool
|
||||
reveal_type(B) # revealed: bool
|
||||
```
|
||||
|
||||
## Integration test: `collections.abc`
|
||||
|
||||
The `collections.abc` standard-library module provides a good integration test, as all its symbols
|
||||
are present due to `*` imports.
|
||||
|
||||
```py
|
||||
import typing
|
||||
import collections.abc
|
||||
|
||||
reveal_type(collections.abc.Sequence) # revealed: Literal[Sequence]
|
||||
reveal_type(collections.abc.Callable) # revealed: typing.Callable
|
||||
```
|
||||
|
||||
## Invalid `*` imports
|
||||
|
||||
### Unresolved module
|
||||
|
||||
If the module is unresolved, we emit a diagnostic just like for any other unresolved import:
|
||||
|
||||
```py
|
||||
# TODO: not a great error message
|
||||
from foo import * # error: [unresolved-import] "Cannot resolve import `foo`"
|
||||
```
|
||||
|
||||
### Nested scope
|
||||
|
||||
A `*` import in a nested scope are always a syntax error. Red-knot does not infer any bindings from
|
||||
them:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
def f():
|
||||
# TODO: we should emit a syntax errror here (tracked by https://github.com/astral-sh/ruff/issues/11934)
|
||||
from a import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `*` combined with other aliases in the list
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
_Y: bool = False
|
||||
_Z: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
<!-- blacken-docs:off -->
|
||||
|
||||
```py
|
||||
from a import *, _Y # error: [invalid-syntax]
|
||||
|
||||
# The import statement above is invalid syntax,
|
||||
# but it's pretty obvious that the user wanted to do a `*` import,
|
||||
# so we import all public names from `a` anyway, to minimize cascading errors
|
||||
reveal_type(X) # revealed: bool
|
||||
reveal_type(_Y) # revealed: bool
|
||||
```
|
||||
|
||||
These tests are more to assert that we don't panic on these various kinds of invalid syntax than
|
||||
anything else:
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
from a import *, _Y # error: [invalid-syntax]
|
||||
from a import _Y, *, _Z # error: [invalid-syntax]
|
||||
from a import *, _Y as fooo # error: [invalid-syntax]
|
||||
from a import *, *, _Y # error: [invalid-syntax]
|
||||
```
|
||||
|
||||
<!-- blacken-docs:on -->
|
||||
|
||||
[python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
|
||||
[typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols
|
||||
@@ -1,7 +1,7 @@
|
||||
# Eager scopes
|
||||
|
||||
Some scopes are executed eagerly: references to variables defined in enclosing scopes are resolved
|
||||
_immediately_. This is in constrast to (for instance) function scopes, where those references are
|
||||
_immediately_. This is in contrast to (for instance) function scopes, where those references are
|
||||
resolved when the function is called.
|
||||
|
||||
## Function definitions
|
||||
|
||||
@@ -12,10 +12,7 @@ reveal_type(__file__) # revealed: str | None
|
||||
reveal_type(__loader__) # revealed: LoaderProtocol | None
|
||||
reveal_type(__package__) # revealed: str | None
|
||||
reveal_type(__doc__) # revealed: str | None
|
||||
|
||||
# TODO: Should be `ModuleSpec | None`
|
||||
# (needs support for `*` imports)
|
||||
reveal_type(__spec__) # revealed: Unknown | None
|
||||
reveal_type(__spec__) # revealed: ModuleSpec | None
|
||||
|
||||
reveal_type(__path__) # revealed: @Todo(generics)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ reveal_type(Bar) # revealed: Literal[Bar]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Literal[object]]
|
||||
```
|
||||
|
||||
## Access to attributes declarated in stubs
|
||||
## Access to attributes declared in stubs
|
||||
|
||||
Unlike regular Python modules, stub files often omit the right-hand side in declarations, including
|
||||
in class scope. However, from the perspective of the type checker, we have to treat them as bindings
|
||||
|
||||
@@ -393,4 +393,87 @@ static_assert(is_assignable_to(Never, type[str]))
|
||||
static_assert(is_assignable_to(Never, type[Any]))
|
||||
```
|
||||
|
||||
## Callable
|
||||
|
||||
The examples provided below are only a subset of the possible cases and include the ones with
|
||||
gradual types. The cases with fully static types and using different combinations of parameter kinds
|
||||
are covered in the [subtyping tests](./is_subtype_of.md#callable).
|
||||
|
||||
### Return type
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, Unknown, static_assert, is_assignable_to
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_assignable_to(Callable[[], Any], Callable[[], int]))
|
||||
static_assert(is_assignable_to(Callable[[], int], Callable[[], Any]))
|
||||
|
||||
static_assert(is_assignable_to(Callable[[], int], Callable[[], float]))
|
||||
static_assert(not is_assignable_to(Callable[[], float], Callable[[], int]))
|
||||
```
|
||||
|
||||
The return types should be checked even if the parameter types uses gradual form (`...`).
|
||||
|
||||
```py
|
||||
static_assert(is_assignable_to(Callable[..., int], Callable[..., float]))
|
||||
static_assert(not is_assignable_to(Callable[..., float], Callable[..., int]))
|
||||
```
|
||||
|
||||
And, if there is no return type, the return type is `Unknown`.
|
||||
|
||||
```py
|
||||
static_assert(is_assignable_to(Callable[[], Unknown], Callable[[], int]))
|
||||
static_assert(is_assignable_to(Callable[[], int], Callable[[], Unknown]))
|
||||
```
|
||||
|
||||
### Parameter types
|
||||
|
||||
A `Callable` which uses the gradual form (`...`) for the parameter types is consistent with any
|
||||
input signature.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, static_assert, is_assignable_to
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_assignable_to(Callable[[], None], Callable[..., None]))
|
||||
static_assert(is_assignable_to(Callable[..., None], Callable[..., None]))
|
||||
static_assert(is_assignable_to(Callable[[int, float, str], None], Callable[..., None]))
|
||||
```
|
||||
|
||||
Even if it includes any other parameter kinds.
|
||||
|
||||
```py
|
||||
def positional_only(a: int, b: int, /) -> None: ...
|
||||
def positional_or_keyword(a: int, b: int) -> None: ...
|
||||
def variadic(*args: int) -> None: ...
|
||||
def keyword_only(*, a: int, b: int) -> None: ...
|
||||
def keyword_variadic(**kwargs: int) -> None: ...
|
||||
def mixed(a: int, /, b: int, *args: int, c: int, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
|
||||
```
|
||||
|
||||
And, even if the parameters are unannotated.
|
||||
|
||||
```py
|
||||
def positional_only(a, b, /) -> None: ...
|
||||
def positional_or_keyword(a, b) -> None: ...
|
||||
def variadic(*args) -> None: ...
|
||||
def keyword_only(*, a, b) -> None: ...
|
||||
def keyword_variadic(**kwargs) -> None: ...
|
||||
def mixed(a, /, b, *args, c, **kwargs) -> None: ...
|
||||
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
|
||||
```
|
||||
|
||||
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
|
||||
@@ -68,6 +68,10 @@ static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
|
||||
|
||||
## Callable
|
||||
|
||||
The examples provided below are only a subset of the possible cases and only include the ones with
|
||||
gradual types. The cases with fully static types and using different combinations of parameter kinds
|
||||
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
|
||||
from typing import Any, Callable
|
||||
@@ -94,7 +98,7 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[]
|
||||
And, similarly for parameters with no annotations.
|
||||
|
||||
```py
|
||||
def f2(a, b) -> None:
|
||||
def f2(a, b, /) -> None:
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
|
||||
@@ -115,8 +119,8 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
|
||||
```
|
||||
|
||||
But, a function with either `*args` or `**kwargs` is not gradual equivalent to a callable with `...`
|
||||
as the parameter type.
|
||||
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
|
||||
callable with `...` as the parameter type.
|
||||
|
||||
```py
|
||||
def variadic_args(*args):
|
||||
@@ -129,4 +133,25 @@ static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_arg
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
|
||||
```
|
||||
|
||||
Parameter names, default values, and it's kind should also be considered when checking for gradual
|
||||
equivalence.
|
||||
|
||||
```py
|
||||
def f1(a): ...
|
||||
def f2(b): ...
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
|
||||
|
||||
def f3(a=1): ...
|
||||
def f4(a=2): ...
|
||||
def f5(a): ...
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f5]))
|
||||
|
||||
def f6(a, /): ...
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f6]))
|
||||
```
|
||||
|
||||
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize
|
||||
|
||||
@@ -494,12 +494,34 @@ Return types are covariant.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
from knot_extensions import is_subtype_of, static_assert, TypeOf
|
||||
|
||||
static_assert(is_subtype_of(Callable[[], int], Callable[[], float]))
|
||||
static_assert(not is_subtype_of(Callable[[], float], Callable[[], int]))
|
||||
```
|
||||
|
||||
### Optional return type
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import is_subtype_of, static_assert, TypeOf
|
||||
|
||||
flag: bool = True
|
||||
|
||||
def optional_return_type() -> int | None:
|
||||
if flag:
|
||||
return 1
|
||||
return None
|
||||
|
||||
def required_return_type() -> int:
|
||||
return 1
|
||||
|
||||
static_assert(not is_subtype_of(TypeOf[optional_return_type], TypeOf[required_return_type]))
|
||||
# TypeOf[some_function] is a singleton function-literal type, not a general callable type
|
||||
static_assert(not is_subtype_of(TypeOf[required_return_type], TypeOf[optional_return_type]))
|
||||
static_assert(is_subtype_of(TypeOf[optional_return_type], Callable[[], int | None]))
|
||||
```
|
||||
|
||||
### Parameter types
|
||||
|
||||
Parameter types are contravariant.
|
||||
@@ -507,13 +529,20 @@ Parameter types are contravariant.
|
||||
#### Positional-only
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
from typing import Callable
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf
|
||||
|
||||
def float_param(a: float, /) -> None: ...
|
||||
def int_param(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[int_param], Callable[[int], None]))
|
||||
static_assert(is_subtype_of(TypeOf[float_param], Callable[[float], None]))
|
||||
|
||||
static_assert(not is_subtype_of(Callable[[int], None], TypeOf[int_param]))
|
||||
static_assert(not is_subtype_of(Callable[[float], None], TypeOf[float_param]))
|
||||
```
|
||||
|
||||
Parameter name is not required to be the same for positional-only parameters at the same position:
|
||||
@@ -533,6 +562,10 @@ def multi_param2(b: int, c: bool, a: str, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[multi_param1], Callable[[float, int, str], None]))
|
||||
|
||||
static_assert(not is_subtype_of(Callable[[float, int, str], None], TypeOf[multi_param1]))
|
||||
```
|
||||
|
||||
#### Positional-only with default value
|
||||
@@ -541,7 +574,8 @@ If the parameter has a default value, it's treated as optional. This means that
|
||||
corresponding position in the supertype does not need to have a default value.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
from typing import Callable
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf
|
||||
|
||||
def float_with_default(a: float = 1, /) -> None: ...
|
||||
def int_with_default(a: int = 1, /) -> None: ...
|
||||
@@ -552,6 +586,13 @@ static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], Call
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_without_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[int], None]))
|
||||
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[], None]))
|
||||
static_assert(is_subtype_of(TypeOf[float_with_default], Callable[[float], None]))
|
||||
|
||||
static_assert(not is_subtype_of(Callable[[int], None], TypeOf[int_with_default]))
|
||||
static_assert(not is_subtype_of(Callable[[float], None], TypeOf[float_with_default]))
|
||||
```
|
||||
|
||||
As the parameter itself is optional, it can be omitted in the supertype:
|
||||
|
||||
@@ -17,7 +17,7 @@ use ruff_db::parsed::ParsedModule;
|
||||
/// ## Usage in salsa tracked structs
|
||||
/// It's important that [`AstNodeRef`] fields in salsa tracked structs are tracked fields
|
||||
/// (attributed with `#[tracked`]). It prevents that the tracked struct gets a new ID
|
||||
/// everytime the AST changes, which in turn, invalidates the result of any query
|
||||
/// every time the AST changes, which in turn, invalidates the result of any query
|
||||
/// that takes said tracked struct as a query argument or returns the tracked struct as part of its result.
|
||||
///
|
||||
/// For example, marking the [`AstNodeRef`] as tracked on `Expression`
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use std::fmt;
|
||||
use std::num::NonZeroU32;
|
||||
use std::ops::Deref;
|
||||
|
||||
use compact_str::{CompactString, ToCompactString};
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
|
||||
use crate::{db::Db, module_resolver::file_to_module};
|
||||
|
||||
/// A module name, e.g. `foo.bar`.
|
||||
///
|
||||
/// Always normalized to the absolute form (never a relative module name, i.e., never `.foo`).
|
||||
@@ -206,6 +211,29 @@ impl ModuleName {
|
||||
pub fn ancestors(&self) -> impl Iterator<Item = Self> {
|
||||
std::iter::successors(Some(self.clone()), Self::parent)
|
||||
}
|
||||
|
||||
pub(crate) fn from_import_statement<'db>(
|
||||
db: &'db dyn Db,
|
||||
importing_file: File,
|
||||
node: &'db ast::StmtImportFrom,
|
||||
) -> Result<Self, ModuleNameResolutionError> {
|
||||
let ast::StmtImportFrom {
|
||||
module,
|
||||
level,
|
||||
names: _,
|
||||
range: _,
|
||||
} = node;
|
||||
|
||||
let module = module.as_deref();
|
||||
|
||||
if let Some(level) = NonZeroU32::new(*level) {
|
||||
relative_module_name(db, importing_file, module, level)
|
||||
} else {
|
||||
module
|
||||
.and_then(Self::new)
|
||||
.ok_or(ModuleNameResolutionError::InvalidSyntax)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ModuleName {
|
||||
@@ -234,3 +262,58 @@ impl std::fmt::Display for ModuleName {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a `from .foo import bar` relative import, resolve the relative module
|
||||
/// we're importing `bar` from into an absolute [`ModuleName`]
|
||||
/// using the name of the module we're currently analyzing.
|
||||
///
|
||||
/// - `level` is the number of dots at the beginning of the relative module name:
|
||||
/// - `from .foo.bar import baz` => `level == 1`
|
||||
/// - `from ...foo.bar import baz` => `level == 3`
|
||||
/// - `tail` is the relative module name stripped of all leading dots:
|
||||
/// - `from .foo import bar` => `tail == "foo"`
|
||||
/// - `from ..foo.bar import baz` => `tail == "foo.bar"`
|
||||
fn relative_module_name(
|
||||
db: &dyn Db,
|
||||
importing_file: File,
|
||||
tail: Option<&str>,
|
||||
level: NonZeroU32,
|
||||
) -> Result<ModuleName, ModuleNameResolutionError> {
|
||||
let module = file_to_module(db, importing_file)
|
||||
.ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
|
||||
let mut level = level.get();
|
||||
|
||||
if module.kind().is_package() {
|
||||
level = level.saturating_sub(1);
|
||||
}
|
||||
|
||||
let mut module_name = module
|
||||
.name()
|
||||
.ancestors()
|
||||
.nth(level as usize)
|
||||
.ok_or(ModuleNameResolutionError::TooManyDots)?;
|
||||
|
||||
if let Some(tail) = tail {
|
||||
let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
|
||||
module_name.extend(&tail);
|
||||
}
|
||||
|
||||
Ok(module_name)
|
||||
}
|
||||
|
||||
/// Various ways in which resolving a [`ModuleName`]
|
||||
/// from an [`ast::StmtImport`] or [`ast::StmtImportFrom`] node might fail
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum ModuleNameResolutionError {
|
||||
/// The import statement has invalid syntax
|
||||
InvalidSyntax,
|
||||
|
||||
/// We couldn't resolve the file we're currently analyzing back to a module
|
||||
/// (Only necessary for relative import statements)
|
||||
UnknownCurrentModule,
|
||||
|
||||
/// The relative import statement seems to take us outside of the module search path
|
||||
/// (e.g. our current module is `foo.bar`, and the relative import statement in `foo.bar`
|
||||
/// is `from ....baz import spam`)
|
||||
TooManyDots,
|
||||
}
|
||||
|
||||
@@ -224,6 +224,9 @@ impl SearchPaths {
|
||||
|
||||
let site_packages_paths = match python_path {
|
||||
PythonPath::SysPrefix(sys_prefix, origin) => {
|
||||
tracing::debug!(
|
||||
"Discovering site-packages paths from sys-prefix `{sys_prefix}` ({origin}')"
|
||||
);
|
||||
// TODO: We may want to warn here if the venv's python version is older
|
||||
// than the one resolved in the program settings because it indicates
|
||||
// that the `target-version` is incorrectly configured or that the
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIds;
|
||||
use crate::semantic_index::attribute_assignment::AttributeAssignments;
|
||||
use crate::semantic_index::builder::SemanticIndexBuilder;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionNodeKey};
|
||||
use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions};
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolTable,
|
||||
@@ -29,6 +29,7 @@ pub mod definition;
|
||||
pub mod expression;
|
||||
mod narrowing_constraints;
|
||||
pub(crate) mod predicate;
|
||||
mod re_exports;
|
||||
pub mod symbol;
|
||||
mod use_def;
|
||||
mod visibility_constraints;
|
||||
@@ -136,7 +137,7 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
||||
|
||||
/// Map from a node creating a definition to its definition.
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
|
||||
|
||||
/// Map from a standalone expression to its [`Expression`] ingredient.
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
@@ -235,8 +236,8 @@ impl<'db> SemanticIndex<'db> {
|
||||
|
||||
/// Returns an iterator over the descendent scopes of `scope`.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendentsIter {
|
||||
DescendentsIter::new(self, scope)
|
||||
pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter {
|
||||
DescendantsIter::new(self, scope)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the direct child scopes of `scope`.
|
||||
@@ -250,13 +251,37 @@ impl<'db> SemanticIndex<'db> {
|
||||
AncestorsIter::new(self, scope)
|
||||
}
|
||||
|
||||
/// Returns the [`Definition`] salsa ingredient for `definition_key`.
|
||||
/// Returns the [`definition::Definition`] salsa ingredient(s) for `definition_key`.
|
||||
///
|
||||
/// There will only ever be >1 `Definition` associated with a `definition_key`
|
||||
/// if the definition is created by a wildcard (`*`) import.
|
||||
#[track_caller]
|
||||
pub(crate) fn definition(
|
||||
pub(crate) fn definitions(
|
||||
&self,
|
||||
definition_key: impl Into<DefinitionNodeKey>,
|
||||
) -> &Definitions<'db> {
|
||||
&self.definitions_by_node[&definition_key.into()]
|
||||
}
|
||||
|
||||
/// Returns the [`definition::Definition`] salsa ingredient for `definition_key`.
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// If the number of definitions associated with the key is not exactly 1 and
|
||||
/// the `debug_assertions` feature is enabled, this method will panic.
|
||||
#[track_caller]
|
||||
pub(crate) fn expect_single_definition(
|
||||
&self,
|
||||
definition_key: impl Into<DefinitionNodeKey> + std::fmt::Debug + Copy,
|
||||
) -> Definition<'db> {
|
||||
self.definitions_by_node[&definition_key.into()]
|
||||
let definitions = self.definitions(definition_key);
|
||||
debug_assert_eq!(
|
||||
definitions.len(),
|
||||
1,
|
||||
"Expected exactly one definition to be associated with AST node {definition_key:?} but found {}",
|
||||
definitions.len()
|
||||
);
|
||||
definitions[0]
|
||||
}
|
||||
|
||||
/// Returns the [`Expression`] ingredient for an expression node.
|
||||
@@ -280,7 +305,8 @@ impl<'db> SemanticIndex<'db> {
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Returns the id of the scope that `node` creates. This is different from [`Definition::scope`] which
|
||||
/// Returns the id of the scope that `node` creates.
|
||||
/// This is different from [`definition::Definition::scope`] which
|
||||
/// returns the scope in which that definition is defined in.
|
||||
#[track_caller]
|
||||
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
|
||||
@@ -339,55 +365,55 @@ impl<'a> Iterator for AncestorsIter<'a> {
|
||||
|
||||
impl FusedIterator for AncestorsIter<'_> {}
|
||||
|
||||
pub struct DescendentsIter<'a> {
|
||||
pub struct DescendantsIter<'a> {
|
||||
next_id: FileScopeId,
|
||||
descendents: std::slice::Iter<'a, Scope>,
|
||||
descendants: std::slice::Iter<'a, Scope>,
|
||||
}
|
||||
|
||||
impl<'a> DescendentsIter<'a> {
|
||||
impl<'a> DescendantsIter<'a> {
|
||||
fn new(symbol_table: &'a SemanticIndex, scope_id: FileScopeId) -> Self {
|
||||
let scope = &symbol_table.scopes[scope_id];
|
||||
let scopes = &symbol_table.scopes[scope.descendents()];
|
||||
let scopes = &symbol_table.scopes[scope.descendants()];
|
||||
|
||||
Self {
|
||||
next_id: scope_id + 1,
|
||||
descendents: scopes.iter(),
|
||||
descendants: scopes.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for DescendentsIter<'a> {
|
||||
impl<'a> Iterator for DescendantsIter<'a> {
|
||||
type Item = (FileScopeId, &'a Scope);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let descendent = self.descendents.next()?;
|
||||
let descendant = self.descendants.next()?;
|
||||
let id = self.next_id;
|
||||
self.next_id = self.next_id + 1;
|
||||
|
||||
Some((id, descendent))
|
||||
Some((id, descendant))
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.descendents.size_hint()
|
||||
self.descendants.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for DescendentsIter<'_> {}
|
||||
impl FusedIterator for DescendantsIter<'_> {}
|
||||
|
||||
impl ExactSizeIterator for DescendentsIter<'_> {}
|
||||
impl ExactSizeIterator for DescendantsIter<'_> {}
|
||||
|
||||
pub struct ChildrenIter<'a> {
|
||||
parent: FileScopeId,
|
||||
descendents: DescendentsIter<'a>,
|
||||
descendants: DescendantsIter<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ChildrenIter<'a> {
|
||||
fn new(module_symbol_table: &'a SemanticIndex, parent: FileScopeId) -> Self {
|
||||
let descendents = DescendentsIter::new(module_symbol_table, parent);
|
||||
let descendants = DescendantsIter::new(module_symbol_table, parent);
|
||||
|
||||
Self {
|
||||
parent,
|
||||
descendents,
|
||||
descendants,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,7 +422,7 @@ impl<'a> Iterator for ChildrenIter<'a> {
|
||||
type Item = (FileScopeId, &'a Scope);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.descendents
|
||||
self.descendants
|
||||
.find(|(_, scope)| scope.parent() == Some(self.parent))
|
||||
}
|
||||
}
|
||||
@@ -1155,9 +1181,9 @@ def x():
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
|
||||
let descendents = index.descendent_scopes(FileScopeId::global());
|
||||
let descendants = index.descendent_scopes(FileScopeId::global());
|
||||
assert_eq!(
|
||||
scope_names(descendents, &db, file),
|
||||
scope_names(descendants, &db, file),
|
||||
vec!["Test", "foo", "bar", "baz", "x"]
|
||||
);
|
||||
|
||||
|
||||
@@ -12,19 +12,21 @@ use ruff_python_ast::{self as ast, ExprContext};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
|
||||
use crate::semantic_index::definition::{
|
||||
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionCategory,
|
||||
DefinitionNodeKey, DefinitionNodeRef, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef,
|
||||
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef,
|
||||
WithItemDefinitionNodeRef,
|
||||
DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef,
|
||||
ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, ImportFromDefinitionNodeRef,
|
||||
MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef, WithItemDefinitionNodeRef,
|
||||
};
|
||||
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
||||
use crate::semantic_index::predicate::{
|
||||
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, ScopedPredicateId,
|
||||
};
|
||||
use crate::semantic_index::re_exports::exported_names;
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId,
|
||||
SymbolTableBuilder,
|
||||
@@ -87,7 +89,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
|
||||
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
|
||||
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
imported_modules: FxHashSet<ModuleName>,
|
||||
attribute_assignments: FxHashMap<FileScopeId, AttributeAssignments<'db>>,
|
||||
@@ -147,6 +149,10 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.current_scope_info().file_scope_id
|
||||
}
|
||||
|
||||
fn current_scope_is_global_scope(&self) -> bool {
|
||||
self.scope_stack.len() == 1
|
||||
}
|
||||
|
||||
/// Returns the scope ID of the surrounding class body scope if the current scope
|
||||
/// is a method inside a class body. Returns `None` otherwise, e.g. if the current
|
||||
/// scope is a function body outside of a class, or if the current scope is not a
|
||||
@@ -229,7 +235,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
|
||||
let children_end = self.scopes.next_index();
|
||||
let popped_scope = &mut self.scopes[popped_scope_id];
|
||||
popped_scope.extend_descendents(children_end);
|
||||
popped_scope.extend_descendants(children_end);
|
||||
|
||||
if !popped_scope.is_eager() {
|
||||
return popped_scope_id;
|
||||
@@ -344,17 +350,55 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.current_symbol_table().mark_symbol_used(id);
|
||||
}
|
||||
|
||||
fn add_entry_for_definition_key(&mut self, key: DefinitionNodeKey) -> &mut Definitions<'db> {
|
||||
self.definitions_by_node.entry(key).or_default()
|
||||
}
|
||||
|
||||
/// Add a [`Definition`] associated with the `definition_node` AST node.
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// This method panics if `debug_assertions` are enabled and the `definition_node` AST node
|
||||
/// already has a [`Definition`] associated with it. This is an important invariant to maintain
|
||||
/// for all nodes *except* [`ast::Alias`] nodes representing `*` imports.
|
||||
fn add_definition(
|
||||
&mut self,
|
||||
symbol: ScopedSymbolId,
|
||||
definition_node: impl Into<DefinitionNodeRef<'db>>,
|
||||
definition_node: impl Into<DefinitionNodeRef<'db>> + std::fmt::Debug + Copy,
|
||||
) -> Definition<'db> {
|
||||
let (definition, num_definitions) =
|
||||
self.push_additional_definition(symbol, definition_node);
|
||||
debug_assert_eq!(
|
||||
num_definitions,
|
||||
1,
|
||||
"Attempted to create multiple `Definition`s associated with AST node {definition_node:?}"
|
||||
);
|
||||
definition
|
||||
}
|
||||
|
||||
/// Push a new [`Definition`] onto the list of definitions
|
||||
/// associated with the `definition_node` AST node.
|
||||
///
|
||||
/// Returns a 2-element tuple, where the first element is the newly created [`Definition`]
|
||||
/// and the second element is the number of definitions that are now associated with
|
||||
/// `definition_node`.
|
||||
///
|
||||
/// This method should only be used when adding a definition associated with a `*` import.
|
||||
/// All other nodes can only ever be associated with exactly 1 or 0 [`Definition`]s.
|
||||
/// For any node other than an [`ast::Alias`] representing a `*` import,
|
||||
/// prefer to use `self.add_definition()`, which ensures that this invariant is maintained.
|
||||
fn push_additional_definition(
|
||||
&mut self,
|
||||
symbol: ScopedSymbolId,
|
||||
definition_node: impl Into<DefinitionNodeRef<'db>>,
|
||||
) -> (Definition<'db>, usize) {
|
||||
let definition_node: DefinitionNodeRef<'_> = definition_node.into();
|
||||
#[allow(unsafe_code)]
|
||||
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
|
||||
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
|
||||
let category = kind.category(self.file.is_stub(self.db.upcast()));
|
||||
let is_reexported = kind.is_reexported();
|
||||
|
||||
let definition = Definition::new(
|
||||
self.db,
|
||||
self.file,
|
||||
@@ -365,10 +409,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
countme::Count::default(),
|
||||
);
|
||||
|
||||
let existing_definition = self
|
||||
.definitions_by_node
|
||||
.insert(definition_node.key(), definition);
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
let num_definitions = {
|
||||
let definitions = self.add_entry_for_definition_key(definition_node.key());
|
||||
definitions.push(definition);
|
||||
definitions.len()
|
||||
};
|
||||
|
||||
if category.is_binding() {
|
||||
self.mark_symbol_bound(symbol);
|
||||
@@ -390,7 +435,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
try_node_stack_manager.record_definition(self);
|
||||
self.try_node_context_stack_manager = try_node_stack_manager;
|
||||
|
||||
definition
|
||||
(definition, num_definitions)
|
||||
}
|
||||
|
||||
fn record_expression_narrowing_constraint(
|
||||
@@ -767,9 +812,10 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
// Insert a mapping from the inner Parameter node to the same definition. This
|
||||
// ensures that calling `HasType::inferred_type` on the inner parameter returns
|
||||
// a valid type (and doesn't panic)
|
||||
let existing_definition = self
|
||||
.definitions_by_node
|
||||
.insert((¶meter.parameter).into(), definition);
|
||||
let existing_definition = self.definitions_by_node.insert(
|
||||
(¶meter.parameter).into(),
|
||||
Definitions::single(definition),
|
||||
);
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
}
|
||||
|
||||
@@ -926,9 +972,6 @@ where
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
|
||||
let symbol = self.add_symbol(class.name.id.clone());
|
||||
self.add_definition(symbol, class);
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::ClassTypeParameters(class),
|
||||
class.type_params.as_deref(),
|
||||
@@ -943,6 +986,10 @@ where
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
|
||||
// In Python runtime semantics, a class is registered after its scope is evaluated.
|
||||
let symbol = self.add_symbol(class.name.id.clone());
|
||||
self.add_definition(symbol, class);
|
||||
}
|
||||
ast::Stmt::TypeAlias(type_alias) => {
|
||||
let symbol = self.add_symbol(
|
||||
@@ -957,7 +1004,7 @@ where
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::TypeAliasTypeParameters(type_alias),
|
||||
type_alias.type_params.as_ref(),
|
||||
type_alias.type_params.as_deref(),
|
||||
|builder| {
|
||||
builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias));
|
||||
builder.visit_expr(&type_alias.value);
|
||||
@@ -990,7 +1037,54 @@ where
|
||||
}
|
||||
}
|
||||
ast::Stmt::ImportFrom(node) => {
|
||||
let mut found_star = false;
|
||||
for (alias_index, alias) in node.names.iter().enumerate() {
|
||||
if &alias.name == "*" {
|
||||
// The following line maintains the invariant that every AST node that
|
||||
// implements `Into<DefinitionNodeKey>` must have an entry in the
|
||||
// `definitions_by_node` map. Maintaining this invariant ensures that
|
||||
// `SemanticIndex::definitions` can always look up the definitions for a
|
||||
// given AST node without panicking.
|
||||
//
|
||||
// The reason why maintaining this invariant requires special handling here
|
||||
// is that some `Alias` nodes may be associated with 0 definitions:
|
||||
// - If the import statement has invalid syntax: multiple `*` names in the `names` list
|
||||
// (e.g. `from foo import *, bar, *`)
|
||||
// - If the `*` import refers to a module that has 0 exported names.
|
||||
// - If the module being imported from cannot be resolved.
|
||||
self.add_entry_for_definition_key(alias.into());
|
||||
|
||||
if found_star {
|
||||
continue;
|
||||
}
|
||||
|
||||
found_star = true;
|
||||
|
||||
// Wildcard imports are invalid syntax everywhere except the top-level scope,
|
||||
// and thus do not bind any definitions anywhere else
|
||||
if !self.current_scope_is_global_scope() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(module_name) =
|
||||
ModuleName::from_import_statement(self.db, self.file, node)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(module) = resolve_module(self.db, &module_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for export in exported_names(self.db, module.file()) {
|
||||
let symbol_id = self.add_symbol(export.clone());
|
||||
let node_ref = StarImportDefinitionNodeRef { node, symbol_id };
|
||||
self.push_additional_definition(symbol_id, node_ref);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
|
||||
(&asname.id, asname.id == alias.name.id)
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_python_ast as ast;
|
||||
@@ -52,10 +54,42 @@ impl<'db> Definition<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// One or more [`Definition`]s.
|
||||
#[derive(Debug, Default, PartialEq, Eq, salsa::Update)]
|
||||
pub struct Definitions<'db>(smallvec::SmallVec<[Definition<'db>; 1]>);
|
||||
|
||||
impl<'db> Definitions<'db> {
|
||||
pub(crate) fn single(definition: Definition<'db>) -> Self {
|
||||
Self(smallvec::smallvec![definition])
|
||||
}
|
||||
|
||||
pub(crate) fn push(&mut self, definition: Definition<'db>) {
|
||||
self.0.push(definition);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Deref for Definitions<'db> {
|
||||
type Target = [Definition<'db>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a Definitions<'db> {
|
||||
type Item = &'a Definition<'db>;
|
||||
type IntoIter = std::slice::Iter<'a, Definition<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum DefinitionNodeRef<'a> {
|
||||
Import(ImportDefinitionNodeRef<'a>),
|
||||
ImportFrom(ImportFromDefinitionNodeRef<'a>),
|
||||
ImportStar(StarImportDefinitionNodeRef<'a>),
|
||||
For(ForStmtDefinitionNodeRef<'a>),
|
||||
Function(&'a ast::StmtFunctionDef),
|
||||
Class(&'a ast::StmtClassDef),
|
||||
@@ -178,12 +212,24 @@ impl<'a> From<MatchPatternDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<StarImportDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node: StarImportDefinitionNodeRef<'a>) -> Self {
|
||||
Self::ImportStar(node)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ImportDefinitionNodeRef<'a> {
|
||||
pub(crate) alias: &'a ast::Alias,
|
||||
pub(crate) is_reexported: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct StarImportDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::StmtImportFrom,
|
||||
pub(crate) symbol_id: ScopedSymbolId,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::StmtImportFrom,
|
||||
@@ -253,6 +299,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
alias: AstNodeRef::new(parsed, alias),
|
||||
is_reexported,
|
||||
}),
|
||||
|
||||
DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef {
|
||||
node,
|
||||
alias_index,
|
||||
@@ -262,6 +309,13 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
alias_index,
|
||||
is_reexported,
|
||||
}),
|
||||
DefinitionNodeRef::ImportStar(star_import) => {
|
||||
let StarImportDefinitionNodeRef { node, symbol_id } = star_import;
|
||||
DefinitionKind::StarImport(StarImportDefinitionKind {
|
||||
node: AstNodeRef::new(parsed, node),
|
||||
symbol_id,
|
||||
})
|
||||
}
|
||||
DefinitionNodeRef::Function(function) => {
|
||||
DefinitionKind::Function(AstNodeRef::new(parsed, function))
|
||||
}
|
||||
@@ -376,6 +430,19 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
alias_index,
|
||||
is_reexported: _,
|
||||
}) => (&node.names[alias_index]).into(),
|
||||
|
||||
// INVARIANT: for an invalid-syntax statement such as `from foo import *, bar, *`,
|
||||
// we only create a `StarImportDefinitionKind` for the *first* `*` alias in the names list.
|
||||
Self::ImportStar(StarImportDefinitionNodeRef { node, symbol_id: _ }) => node
|
||||
.names
|
||||
.iter()
|
||||
.find(|alias| &alias.name == "*")
|
||||
.expect(
|
||||
"The `StmtImportFrom` node of a `StarImportDefinitionKind` instance \
|
||||
should always have at least one `alias` with the name `*`.",
|
||||
)
|
||||
.into(),
|
||||
|
||||
Self::Function(node) => node.into(),
|
||||
Self::Class(node) => node.into(),
|
||||
Self::TypeAlias(node) => node.into(),
|
||||
@@ -463,6 +530,7 @@ impl DefinitionCategory {
|
||||
pub enum DefinitionKind<'db> {
|
||||
Import(ImportDefinitionKind),
|
||||
ImportFrom(ImportFromDefinitionKind),
|
||||
StarImport(StarImportDefinitionKind),
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
Class(AstNodeRef<ast::StmtClassDef>),
|
||||
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
|
||||
@@ -492,6 +560,13 @@ impl DefinitionKind<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn as_star_import(&self) -> Option<&StarImportDefinitionKind> {
|
||||
match self {
|
||||
DefinitionKind::StarImport(import) => Some(import),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`TextRange`] of the definition target.
|
||||
///
|
||||
/// A definition target would mainly be the node representing the symbol being defined i.e.,
|
||||
@@ -502,6 +577,7 @@ impl DefinitionKind<'_> {
|
||||
match self {
|
||||
DefinitionKind::Import(import) => import.alias().range(),
|
||||
DefinitionKind::ImportFrom(import) => import.alias().range(),
|
||||
DefinitionKind::StarImport(import) => import.alias().range(),
|
||||
DefinitionKind::Function(function) => function.name.range(),
|
||||
DefinitionKind::Class(class) => class.name.range(),
|
||||
DefinitionKind::TypeAlias(type_alias) => type_alias.name.range(),
|
||||
@@ -531,6 +607,7 @@ impl DefinitionKind<'_> {
|
||||
| DefinitionKind::TypeAlias(_)
|
||||
| DefinitionKind::Import(_)
|
||||
| DefinitionKind::ImportFrom(_)
|
||||
| DefinitionKind::StarImport(_)
|
||||
| DefinitionKind::TypeVar(_)
|
||||
| DefinitionKind::ParamSpec(_)
|
||||
| DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding,
|
||||
@@ -589,7 +666,36 @@ impl<'db> From<Option<Unpack<'db>>> for TargetKind<'db> {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct StarImportDefinitionKind {
|
||||
node: AstNodeRef<ast::StmtImportFrom>,
|
||||
symbol_id: ScopedSymbolId,
|
||||
}
|
||||
|
||||
impl StarImportDefinitionKind {
|
||||
pub(crate) fn import(&self) -> &ast::StmtImportFrom {
|
||||
self.node.node()
|
||||
}
|
||||
|
||||
pub(crate) fn alias(&self) -> &ast::Alias {
|
||||
// INVARIANT: for an invalid-syntax statement such as `from foo import *, bar, *`,
|
||||
// we only create a `StarImportDefinitionKind` for the *first* `*` alias in the names list.
|
||||
self.node
|
||||
.node()
|
||||
.names
|
||||
.iter()
|
||||
.find(|alias| &alias.name == "*")
|
||||
.expect(
|
||||
"The `StmtImportFrom` node of a `StarImportDefinitionKind` instance \
|
||||
should always have at least one `alias` with the name `*`.",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn symbol_id(&self) -> ScopedSymbolId {
|
||||
self.symbol_id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MatchPatternDefinitionKind {
|
||||
pattern: AstNodeRef<ast::Pattern>,
|
||||
identifier: AstNodeRef<ast::Identifier>,
|
||||
|
||||
360
crates/red_knot_python_semantic/src/semantic_index/re_exports.rs
Normal file
360
crates/red_knot_python_semantic/src/semantic_index/re_exports.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
//! A visitor and query to find all global-scope symbols that are exported from a module
|
||||
//! when a wildcard import is used.
|
||||
//!
|
||||
//! For example, if a module `foo` contains `from bar import *`, which symbols from the global
|
||||
//! scope of `bar` are imported into the global namespace of `foo`?
|
||||
//!
|
||||
//! ## Why is this a separate query rather than a part of semantic indexing?
|
||||
//!
|
||||
//! This query is called by the [`super::SemanticIndexBuilder`] in order to add the correct
|
||||
//! [`super::Definition`]s to the semantic index of a module `foo` if `foo` has a
|
||||
//! `from bar import *` statement in its global namespace. Adding the correct `Definition`s to
|
||||
//! `foo`'s [`super::SemanticIndex`] requires knowing which symbols are exported from `bar`.
|
||||
//!
|
||||
//! If we determined the set of exported names during semantic indexing rather than as a
|
||||
//! separate query, we would need to complete semantic indexing on `bar` in order to
|
||||
//! complete analysis of the global namespace of `foo`. Since semantic indexing is somewhat
|
||||
//! expensive, this would be undesirable. A separate query allows us to avoid this issue.
|
||||
//!
|
||||
//! An additional concern is that the recursive nature of this query means that it must be able
|
||||
//! to handle cycles. We do this using fixpoint iteration; adding fixpoint iteration to the
|
||||
//! whole [`super::semantic_index()`] query would probably be prohibitively expensive.
|
||||
|
||||
use ruff_db::{files::File, parsed::parsed_module};
|
||||
use ruff_python_ast::{
|
||||
self as ast,
|
||||
name::Name,
|
||||
visitor::{walk_expr, walk_pattern, walk_stmt, Visitor},
|
||||
};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::{module_name::ModuleName, resolve_module, Db};
|
||||
|
||||
fn exports_cycle_recover(
|
||||
_db: &dyn Db,
|
||||
_value: &FxHashSet<Name>,
|
||||
_count: u32,
|
||||
_file: File,
|
||||
) -> salsa::CycleRecoveryAction<FxHashSet<Name>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn exports_cycle_initial(_db: &dyn Db, _file: File) -> FxHashSet<Name> {
|
||||
FxHashSet::default()
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref, cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial)]
|
||||
pub(super) fn exported_names(db: &dyn Db, file: File) -> FxHashSet<Name> {
|
||||
let module = parsed_module(db.upcast(), file);
|
||||
let mut finder = ExportFinder::new(db, file);
|
||||
finder.visit_body(module.suite());
|
||||
finder.exports
|
||||
}
|
||||
|
||||
struct ExportFinder<'db> {
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
visiting_stub_file: bool,
|
||||
exports: FxHashSet<Name>,
|
||||
}
|
||||
|
||||
impl<'db> ExportFinder<'db> {
|
||||
fn new(db: &'db dyn Db, file: File) -> Self {
|
||||
Self {
|
||||
db,
|
||||
file,
|
||||
visiting_stub_file: file.is_stub(db.upcast()),
|
||||
exports: FxHashSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn possibly_add_export(&mut self, name: &Name) {
|
||||
if name.starts_with('_') {
|
||||
return;
|
||||
}
|
||||
self.exports.insert(name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Visitor<'db> for ExportFinder<'db> {
|
||||
fn visit_alias(&mut self, alias: &'db ast::Alias) {
|
||||
let ast::Alias { name, asname, .. } = alias;
|
||||
if self.visiting_stub_file {
|
||||
// If the source is a stub, names defined by imports are only exported
|
||||
// if they use the explicit `foo as foo` syntax:
|
||||
if asname.as_ref().is_some_and(|asname| asname.id == name.id) {
|
||||
self.possibly_add_export(&name.id);
|
||||
}
|
||||
} else {
|
||||
self.possibly_add_export(&asname.as_ref().unwrap_or(name).id);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &'db ast::Pattern) {
|
||||
match pattern {
|
||||
ast::Pattern::MatchAs(ast::PatternMatchAs {
|
||||
pattern,
|
||||
name,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(pattern) = pattern {
|
||||
self.visit_pattern(pattern);
|
||||
}
|
||||
if let Some(name) = name {
|
||||
// Wildcard patterns (`case _:`) do not bind names.
|
||||
// Currently `self.possibly_add_export()` just ignores
|
||||
// all names with leading underscores, but this will not always be the case
|
||||
// (in the future we will want to support modules with `__all__ = ['_']`).
|
||||
if name != "_" {
|
||||
self.possibly_add_export(&name.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Pattern::MatchMapping(ast::PatternMatchMapping {
|
||||
patterns,
|
||||
rest,
|
||||
keys: _,
|
||||
range: _,
|
||||
}) => {
|
||||
for pattern in patterns {
|
||||
self.visit_pattern(pattern);
|
||||
}
|
||||
if let Some(rest) = rest {
|
||||
self.possibly_add_export(&rest.id);
|
||||
}
|
||||
}
|
||||
ast::Pattern::MatchStar(ast::PatternMatchStar { name, range: _ }) => {
|
||||
if let Some(name) = name {
|
||||
self.possibly_add_export(&name.id);
|
||||
}
|
||||
}
|
||||
ast::Pattern::MatchSequence(_)
|
||||
| ast::Pattern::MatchOr(_)
|
||||
| ast::Pattern::MatchClass(_) => {
|
||||
walk_pattern(self, pattern);
|
||||
}
|
||||
ast::Pattern::MatchSingleton(_) | ast::Pattern::MatchValue(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &'db ruff_python_ast::Stmt) {
|
||||
match stmt {
|
||||
ast::Stmt::ClassDef(ast::StmtClassDef {
|
||||
name,
|
||||
decorator_list,
|
||||
arguments,
|
||||
type_params: _, // We don't want to visit the type params of the class
|
||||
body: _, // We don't want to visit the body of the class
|
||||
range: _,
|
||||
}) => {
|
||||
self.possibly_add_export(&name.id);
|
||||
for decorator in decorator_list {
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
if let Some(arguments) = arguments {
|
||||
self.visit_arguments(arguments);
|
||||
}
|
||||
}
|
||||
ast::Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
name,
|
||||
decorator_list,
|
||||
parameters,
|
||||
returns,
|
||||
type_params: _, // We don't want to visit the type params of the function
|
||||
body: _, // We don't want to visit the body of the function
|
||||
range: _,
|
||||
is_async: _,
|
||||
}) => {
|
||||
self.possibly_add_export(&name.id);
|
||||
for decorator in decorator_list {
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
self.visit_parameters(parameters);
|
||||
if let Some(returns) = returns {
|
||||
self.visit_expr(returns);
|
||||
}
|
||||
}
|
||||
ast::Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
target,
|
||||
value,
|
||||
annotation,
|
||||
simple: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if value.is_some() || self.visiting_stub_file {
|
||||
self.visit_expr(target);
|
||||
}
|
||||
self.visit_expr(annotation);
|
||||
if let Some(value) = value {
|
||||
self.visit_expr(value);
|
||||
}
|
||||
}
|
||||
ast::Stmt::TypeAlias(ast::StmtTypeAlias {
|
||||
name,
|
||||
type_params: _,
|
||||
value: _,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_expr(name);
|
||||
// Neither walrus expressions nor statements cannot appear in type aliases;
|
||||
// no need to recursively visit the `value` or `type_params`
|
||||
}
|
||||
ast::Stmt::ImportFrom(node) => {
|
||||
let mut found_star = false;
|
||||
for name in &node.names {
|
||||
if &name.name.id == "*" {
|
||||
if !found_star {
|
||||
found_star = true;
|
||||
self.exports.extend(
|
||||
ModuleName::from_import_statement(self.db, self.file, node)
|
||||
.ok()
|
||||
.and_then(|module_name| resolve_module(self.db, &module_name))
|
||||
.iter()
|
||||
.flat_map(|module| exported_names(self.db, module.file()))
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.visit_alias(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::Import(_)
|
||||
| ast::Stmt::AugAssign(_)
|
||||
| ast::Stmt::While(_)
|
||||
| ast::Stmt::If(_)
|
||||
| ast::Stmt::With(_)
|
||||
| ast::Stmt::Assert(_)
|
||||
| ast::Stmt::Try(_)
|
||||
| ast::Stmt::Expr(_)
|
||||
| ast::Stmt::For(_)
|
||||
| ast::Stmt::Assign(_)
|
||||
| ast::Stmt::Match(_) => walk_stmt(self, stmt),
|
||||
|
||||
ast::Stmt::Global(_)
|
||||
| ast::Stmt::Raise(_)
|
||||
| ast::Stmt::Return(_)
|
||||
| ast::Stmt::Break(_)
|
||||
| ast::Stmt::Continue(_)
|
||||
| ast::Stmt::IpyEscapeCommand(_)
|
||||
| ast::Stmt::Delete(_)
|
||||
| ast::Stmt::Nonlocal(_)
|
||||
| ast::Stmt::Pass(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'db ast::Expr) {
|
||||
match expr {
|
||||
ast::Expr::Name(ast::ExprName { id, ctx, range: _ }) => {
|
||||
if ctx.is_store() {
|
||||
self.possibly_add_export(id);
|
||||
}
|
||||
}
|
||||
|
||||
ast::Expr::Lambda(_)
|
||||
| ast::Expr::BooleanLiteral(_)
|
||||
| ast::Expr::NoneLiteral(_)
|
||||
| ast::Expr::NumberLiteral(_)
|
||||
| ast::Expr::BytesLiteral(_)
|
||||
| ast::Expr::EllipsisLiteral(_)
|
||||
| ast::Expr::StringLiteral(_) => {}
|
||||
|
||||
// Walrus definitions "leak" from comprehension scopes into the comprehension's
|
||||
// enclosing scope; they thus need special handling
|
||||
ast::Expr::SetComp(_)
|
||||
| ast::Expr::ListComp(_)
|
||||
| ast::Expr::Generator(_)
|
||||
| ast::Expr::DictComp(_) => {
|
||||
let mut walrus_finder = WalrusFinder {
|
||||
export_finder: self,
|
||||
};
|
||||
walk_expr(&mut walrus_finder, expr);
|
||||
}
|
||||
|
||||
ast::Expr::BoolOp(_)
|
||||
| ast::Expr::Named(_)
|
||||
| ast::Expr::BinOp(_)
|
||||
| ast::Expr::UnaryOp(_)
|
||||
| ast::Expr::If(_)
|
||||
| ast::Expr::Attribute(_)
|
||||
| ast::Expr::Subscript(_)
|
||||
| ast::Expr::Starred(_)
|
||||
| ast::Expr::Call(_)
|
||||
| ast::Expr::Compare(_)
|
||||
| ast::Expr::Yield(_)
|
||||
| ast::Expr::YieldFrom(_)
|
||||
| ast::Expr::FString(_)
|
||||
| ast::Expr::Tuple(_)
|
||||
| ast::Expr::List(_)
|
||||
| ast::Expr::Slice(_)
|
||||
| ast::Expr::IpyEscapeCommand(_)
|
||||
| ast::Expr::Dict(_)
|
||||
| ast::Expr::Set(_)
|
||||
| ast::Expr::Await(_) => walk_expr(self, expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WalrusFinder<'a, 'db> {
|
||||
export_finder: &'a mut ExportFinder<'db>,
|
||||
}
|
||||
|
||||
impl<'db> Visitor<'db> for WalrusFinder<'_, 'db> {
|
||||
fn visit_expr(&mut self, expr: &'db ast::Expr) {
|
||||
match expr {
|
||||
// It's important for us to short-circuit here for lambdas specifically,
|
||||
// as walruses cannot leak out of the body of a lambda function.
|
||||
ast::Expr::Lambda(_)
|
||||
| ast::Expr::BooleanLiteral(_)
|
||||
| ast::Expr::NoneLiteral(_)
|
||||
| ast::Expr::NumberLiteral(_)
|
||||
| ast::Expr::BytesLiteral(_)
|
||||
| ast::Expr::EllipsisLiteral(_)
|
||||
| ast::Expr::StringLiteral(_)
|
||||
| ast::Expr::Name(_) => {}
|
||||
|
||||
ast::Expr::Named(ast::ExprNamed {
|
||||
target,
|
||||
value: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let ast::Expr::Name(ast::ExprName {
|
||||
id,
|
||||
ctx: ast::ExprContext::Store,
|
||||
range: _,
|
||||
}) = &**target
|
||||
{
|
||||
self.export_finder.possibly_add_export(id);
|
||||
}
|
||||
}
|
||||
|
||||
// We must recurse inside nested comprehensions,
|
||||
// as even a walrus inside a comprehension inside a comprehension in the global scope
|
||||
// will leak out into the global scope
|
||||
ast::Expr::DictComp(_)
|
||||
| ast::Expr::SetComp(_)
|
||||
| ast::Expr::ListComp(_)
|
||||
| ast::Expr::Generator(_)
|
||||
| ast::Expr::BoolOp(_)
|
||||
| ast::Expr::BinOp(_)
|
||||
| ast::Expr::UnaryOp(_)
|
||||
| ast::Expr::If(_)
|
||||
| ast::Expr::Attribute(_)
|
||||
| ast::Expr::Subscript(_)
|
||||
| ast::Expr::Starred(_)
|
||||
| ast::Expr::Call(_)
|
||||
| ast::Expr::Compare(_)
|
||||
| ast::Expr::Yield(_)
|
||||
| ast::Expr::YieldFrom(_)
|
||||
| ast::Expr::FString(_)
|
||||
| ast::Expr::Tuple(_)
|
||||
| ast::Expr::List(_)
|
||||
| ast::Expr::Slice(_)
|
||||
| ast::Expr::IpyEscapeCommand(_)
|
||||
| ast::Expr::Dict(_)
|
||||
| ast::Expr::Set(_)
|
||||
| ast::Expr::Await(_) => walk_expr(self, expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,19 +171,19 @@ impl FileScopeId {
|
||||
pub struct Scope {
|
||||
parent: Option<FileScopeId>,
|
||||
node: NodeWithScopeKind,
|
||||
descendents: Range<FileScopeId>,
|
||||
descendants: Range<FileScopeId>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub(super) fn new(
|
||||
parent: Option<FileScopeId>,
|
||||
node: NodeWithScopeKind,
|
||||
descendents: Range<FileScopeId>,
|
||||
descendants: Range<FileScopeId>,
|
||||
) -> Self {
|
||||
Scope {
|
||||
parent,
|
||||
node,
|
||||
descendents,
|
||||
descendants,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,12 +199,12 @@ impl Scope {
|
||||
self.node().scope_kind()
|
||||
}
|
||||
|
||||
pub fn descendents(&self) -> Range<FileScopeId> {
|
||||
self.descendents.clone()
|
||||
pub fn descendants(&self) -> Range<FileScopeId> {
|
||||
self.descendants.clone()
|
||||
}
|
||||
|
||||
pub(super) fn extend_descendents(&mut self, children_end: FileScopeId) {
|
||||
self.descendents = self.descendents.start..children_end;
|
||||
pub(super) fn extend_descendants(&mut self, children_end: FileScopeId) {
|
||||
self.descendants = self.descendants.start..children_end;
|
||||
}
|
||||
|
||||
pub(crate) fn is_eager(&self) -> bool {
|
||||
|
||||
@@ -149,7 +149,7 @@ macro_rules! impl_binding_has_ty {
|
||||
#[inline]
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let index = semantic_index(model.db, model.file);
|
||||
let binding = index.definition(self);
|
||||
let binding = index.expect_single_definition(self);
|
||||
binding_type(model.db, binding)
|
||||
}
|
||||
}
|
||||
@@ -158,10 +158,19 @@ macro_rules! impl_binding_has_ty {
|
||||
|
||||
impl_binding_has_ty!(ast::StmtFunctionDef);
|
||||
impl_binding_has_ty!(ast::StmtClassDef);
|
||||
impl_binding_has_ty!(ast::Alias);
|
||||
impl_binding_has_ty!(ast::Parameter);
|
||||
impl_binding_has_ty!(ast::ParameterWithDefault);
|
||||
|
||||
impl HasType for ast::Alias {
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
if &self.name == "*" {
|
||||
return Type::Never;
|
||||
}
|
||||
let index = semantic_index(model.db, model.file);
|
||||
binding_type(model.db, index.expect_single_definition(self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -110,25 +110,14 @@ impl<'db> UnionBuilder<'db> {
|
||||
return self.collapse_to_object();
|
||||
}
|
||||
}
|
||||
match to_remove[..] {
|
||||
[] => self.elements.push(to_add),
|
||||
[index] => self.elements[index] = to_add,
|
||||
_ => {
|
||||
let mut current_index = 0;
|
||||
let mut to_remove = to_remove.into_iter();
|
||||
let mut next_to_remove_index = to_remove.next();
|
||||
self.elements.retain(|_| {
|
||||
let retain = if Some(current_index) == next_to_remove_index {
|
||||
next_to_remove_index = to_remove.next();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
current_index += 1;
|
||||
retain
|
||||
});
|
||||
self.elements.push(to_add);
|
||||
if let Some((&first, rest)) = to_remove.split_first() {
|
||||
self.elements[first] = to_add;
|
||||
// We iterate in descending order to keep remaining indices valid after `swap_remove`.
|
||||
for &index in rest.iter().rev() {
|
||||
self.elements.swap_remove(index);
|
||||
}
|
||||
} else {
|
||||
self.elements.push(to_add);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ use crate::Db;
|
||||
|
||||
mod arguments;
|
||||
mod bind;
|
||||
pub(super) use arguments::{Argument, CallArguments};
|
||||
pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments};
|
||||
pub(super) use bind::Bindings;
|
||||
|
||||
/// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was
|
||||
/// unsuccessful.
|
||||
///
|
||||
/// The bindings are boxed so that we do not pass around large `Err` variants on the stack.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CallError<'db>(pub(crate) CallErrorKind, pub(crate) Box<Bindings<'db>>);
|
||||
|
||||
/// The reason why calling a type failed.
|
||||
@@ -32,7 +32,7 @@ pub(crate) enum CallErrorKind {
|
||||
PossiblyNotCallable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub(super) enum CallDunderError<'db> {
|
||||
/// The dunder attribute exists but it can't be called with the given arguments.
|
||||
///
|
||||
|
||||
@@ -1,88 +1,128 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use super::Type;
|
||||
|
||||
/// Typed arguments for a single call, in source order.
|
||||
/// Arguments for a single call, in source order.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
|
||||
pub(crate) struct CallArguments<'a>(VecDeque<Argument<'a>>);
|
||||
|
||||
impl<'a, 'db> CallArguments<'a, 'db> {
|
||||
/// Create a [`CallArguments`] with no arguments.
|
||||
pub(crate) fn none() -> Self {
|
||||
Self(Vec::new())
|
||||
impl<'a> CallArguments<'a> {
|
||||
/// Invoke a function with an optional extra synthetic argument (for a `self` or `cls`
|
||||
/// parameter) prepended to the front of this argument list. (If `bound_self` is none, the
|
||||
/// function is invoked with the unmodified argument list.)
|
||||
pub(crate) fn with_self<F, R>(&mut self, bound_self: Option<Type<'_>>, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
{
|
||||
if bound_self.is_some() {
|
||||
self.0.push_front(Argument::Synthetic);
|
||||
}
|
||||
let result = f(self);
|
||||
if bound_self.is_some() {
|
||||
self.0.pop_front();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
|
||||
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
|
||||
positional_tys
|
||||
.into_iter()
|
||||
.map(Argument::Positional)
|
||||
.collect()
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Prepend an extra positional argument.
|
||||
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
|
||||
let mut arguments = Vec::with_capacity(self.0.len() + 1);
|
||||
arguments.push(Argument::Synthetic(self_ty));
|
||||
arguments.extend_from_slice(&self.0);
|
||||
Self(arguments)
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.first().map(Argument::ty)
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.get(1).map(Argument::ty)
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn third_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.get(2).map(Argument::ty)
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = Argument<'a>> + '_ {
|
||||
self.0.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
|
||||
type Item = &'b Argument<'a, 'db>;
|
||||
type IntoIter = std::slice::Iter<'b, Argument<'a, 'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> FromIterator<Argument<'a, 'db>> for CallArguments<'a, 'db> {
|
||||
fn from_iter<T: IntoIterator<Item = Argument<'a, 'db>>>(iter: T) -> Self {
|
||||
impl<'a> FromIterator<Argument<'a>> for CallArguments<'a> {
|
||||
fn from_iter<T: IntoIterator<Item = Argument<'a>>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Argument<'a, 'db> {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum Argument<'a> {
|
||||
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
|
||||
Synthetic(Type<'db>),
|
||||
Synthetic,
|
||||
/// A positional argument.
|
||||
Positional(Type<'db>),
|
||||
Positional,
|
||||
/// A starred positional argument (e.g. `*args`).
|
||||
Variadic(Type<'db>),
|
||||
Variadic,
|
||||
/// A keyword argument (e.g. `a=1`).
|
||||
Keyword { name: &'a str, ty: Type<'db> },
|
||||
Keyword(&'a str),
|
||||
/// The double-starred keywords argument (e.g. `**kwargs`).
|
||||
Keywords(Type<'db>),
|
||||
Keywords,
|
||||
}
|
||||
|
||||
impl<'db> Argument<'_, 'db> {
|
||||
fn ty(&self) -> Type<'db> {
|
||||
match self {
|
||||
Self::Synthetic(ty) => *ty,
|
||||
Self::Positional(ty) => *ty,
|
||||
Self::Variadic(ty) => *ty,
|
||||
Self::Keyword { name: _, ty } => *ty,
|
||||
Self::Keywords(ty) => *ty,
|
||||
/// Arguments for a single call, in source order, along with inferred types for each argument.
|
||||
pub(crate) struct CallArgumentTypes<'a, 'db> {
|
||||
arguments: CallArguments<'a>,
|
||||
types: VecDeque<Type<'db>>,
|
||||
}
|
||||
|
||||
impl<'a, 'db> CallArgumentTypes<'a, 'db> {
|
||||
/// Create a [`CallArgumentTypes`] with no arguments.
|
||||
pub(crate) fn none() -> Self {
|
||||
let arguments = CallArguments::default();
|
||||
let types = VecDeque::default();
|
||||
Self { arguments, types }
|
||||
}
|
||||
|
||||
/// Create a [`CallArgumentTypes`] from an iterator over non-variadic positional argument
|
||||
/// types.
|
||||
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
|
||||
let types: VecDeque<_> = positional_tys.into_iter().collect();
|
||||
let arguments = CallArguments(vec![Argument::Positional; types.len()].into());
|
||||
Self { arguments, types }
|
||||
}
|
||||
|
||||
/// Create a new [`CallArgumentTypes`] to store the inferred types of the arguments in a
|
||||
/// [`CallArguments`]. Uses the provided callback to infer each argument type.
|
||||
pub(crate) fn new<F>(arguments: CallArguments<'a>, mut f: F) -> Self
|
||||
where
|
||||
F: FnMut(usize, Argument<'a>) -> Type<'db>,
|
||||
{
|
||||
let types = arguments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, argument)| f(idx, argument))
|
||||
.collect();
|
||||
Self { arguments, types }
|
||||
}
|
||||
|
||||
/// Invoke a function with an optional extra synthetic argument (for a `self` or `cls`
|
||||
/// parameter) prepended to the front of this argument list. (If `bound_self` is none, the
|
||||
/// function is invoked with the unmodified argument list.)
|
||||
pub(crate) fn with_self<F, R>(&mut self, bound_self: Option<Type<'db>>, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
{
|
||||
if let Some(bound_self) = bound_self {
|
||||
self.arguments.0.push_front(Argument::Synthetic);
|
||||
self.types.push_front(bound_self);
|
||||
}
|
||||
let result = f(self);
|
||||
if bound_self.is_some() {
|
||||
self.arguments.0.pop_front();
|
||||
self.types.pop_front();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
|
||||
self.arguments.iter().zip(self.types.iter().copied())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for CallArgumentTypes<'a, '_> {
|
||||
type Target = CallArguments<'a>;
|
||||
fn deref(&self) -> &CallArguments<'a> {
|
||||
&self.arguments
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for CallArgumentTypes<'a, '_> {
|
||||
fn deref_mut(&mut self) -> &mut CallArguments<'a> {
|
||||
&mut self.arguments
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,24 @@
|
||||
//! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a
|
||||
//! union of types, each of which might contain multiple overloads.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::{
|
||||
Argument, CallArguments, CallError, CallErrorKind, CallableSignature, InferContext, Signature,
|
||||
Signatures, Type,
|
||||
Argument, CallArgumentTypes, CallArguments, CallError, CallErrorKind, CallableSignature,
|
||||
InferContext, Signature, Signatures, Type,
|
||||
};
|
||||
use crate::db::Db;
|
||||
use crate::symbol::{Boundness, Symbol};
|
||||
use crate::types::diagnostic::{
|
||||
CALL_NON_CALLABLE, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD,
|
||||
PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
|
||||
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT,
|
||||
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS,
|
||||
UNKNOWN_ARGUMENT,
|
||||
};
|
||||
use crate::types::signatures::{Parameter, ParameterForm};
|
||||
use crate::types::{
|
||||
todo_type, BoundMethodType, CallableType, ClassLiteralType, KnownClass, KnownFunction,
|
||||
KnownInstanceType, UnionType,
|
||||
};
|
||||
use crate::types::signatures::Parameter;
|
||||
use crate::types::{CallableType, UnionType};
|
||||
use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -26,29 +29,75 @@ use ruff_text_size::Ranged;
|
||||
/// compatible with _all_ of the types in the union for the call to be valid.
|
||||
///
|
||||
/// It's guaranteed that the wrapped bindings have no errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Bindings<'db> {
|
||||
pub(crate) callable_type: Type<'db>,
|
||||
signatures: Signatures<'db>,
|
||||
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a non-union
|
||||
/// type.
|
||||
elements: SmallVec<[CallableBinding<'db>; 1]>,
|
||||
|
||||
/// Whether each argument will be used as a value and/or a type form in this call.
|
||||
pub(crate) argument_forms: Box<[Option<ParameterForm>]>,
|
||||
|
||||
conflicting_forms: Box<[bool]>,
|
||||
}
|
||||
|
||||
impl<'db> Bindings<'db> {
|
||||
/// Binds the arguments of a call site against a signature.
|
||||
/// Match the arguments of a call site against the parameters of a collection of possibly
|
||||
/// unioned, possibly overloaded signatures.
|
||||
///
|
||||
/// The returned bindings provide the return type of the call, the bound types for all
|
||||
/// The returned bindings tell you which parameter (in each signature) each argument was
|
||||
/// matched against. You can then perform type inference on each argument with extra context
|
||||
/// about the expected parameter types. (You do this by creating a [`CallArgumentTypes`] object
|
||||
/// from the `arguments` that you match against.)
|
||||
///
|
||||
/// Once you have argument types available, you can call [`check_types`][Self::check_types] to
|
||||
/// verify that each argument type is assignable to the corresponding parameter type.
|
||||
pub(crate) fn match_parameters(
|
||||
signatures: Signatures<'db>,
|
||||
arguments: &mut CallArguments<'_>,
|
||||
) -> Self {
|
||||
let mut argument_forms = vec![None; arguments.len()];
|
||||
let mut conflicting_forms = vec![false; arguments.len()];
|
||||
let elements: SmallVec<[CallableBinding<'db>; 1]> = signatures
|
||||
.iter()
|
||||
.map(|signature| {
|
||||
CallableBinding::match_parameters(
|
||||
signature,
|
||||
arguments,
|
||||
&mut argument_forms,
|
||||
&mut conflicting_forms,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Bindings {
|
||||
signatures,
|
||||
elements,
|
||||
argument_forms: argument_forms.into(),
|
||||
conflicting_forms: conflicting_forms.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that the type of each argument is assignable to type of the parameter that it was
|
||||
/// matched to.
|
||||
///
|
||||
/// You must provide an `argument_types` that was created from the same `arguments` that you
|
||||
/// provided to [`match_parameters`][Self::match_parameters].
|
||||
///
|
||||
/// We update the bindings to include the return type of the call, the bound types for all
|
||||
/// parameters, and any errors resulting from binding the call, all for each union element and
|
||||
/// overload (if any).
|
||||
pub(crate) fn bind(
|
||||
pub(crate) fn check_types(
|
||||
mut self,
|
||||
db: &'db dyn Db,
|
||||
signatures: &Signatures<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
argument_types: &mut CallArgumentTypes<'_, 'db>,
|
||||
) -> Result<Self, CallError<'db>> {
|
||||
let elements: SmallVec<[CallableBinding<'db>; 1]> = signatures
|
||||
.into_iter()
|
||||
.map(|signature| CallableBinding::bind(db, signature, arguments))
|
||||
.collect();
|
||||
for (signature, element) in self.signatures.iter().zip(&mut self.elements) {
|
||||
element.check_types(db, signature, argument_types);
|
||||
}
|
||||
|
||||
self.evaluate_known_cases(db);
|
||||
|
||||
// In order of precedence:
|
||||
//
|
||||
@@ -68,28 +117,28 @@ impl<'db> Bindings<'db> {
|
||||
let mut all_ok = true;
|
||||
let mut any_binding_error = false;
|
||||
let mut all_not_callable = true;
|
||||
for binding in &elements {
|
||||
if self.conflicting_forms.contains(&true) {
|
||||
all_ok = false;
|
||||
any_binding_error = true;
|
||||
all_not_callable = false;
|
||||
}
|
||||
for binding in &self.elements {
|
||||
let result = binding.as_result();
|
||||
all_ok &= result.is_ok();
|
||||
any_binding_error |= matches!(result, Err(CallErrorKind::BindingError));
|
||||
all_not_callable &= matches!(result, Err(CallErrorKind::NotCallable));
|
||||
}
|
||||
|
||||
let bindings = Bindings {
|
||||
callable_type: signatures.callable_type,
|
||||
elements,
|
||||
};
|
||||
|
||||
if all_ok {
|
||||
Ok(bindings)
|
||||
Ok(self)
|
||||
} else if any_binding_error {
|
||||
Err(CallError(CallErrorKind::BindingError, Box::new(bindings)))
|
||||
Err(CallError(CallErrorKind::BindingError, Box::new(self)))
|
||||
} else if all_not_callable {
|
||||
Err(CallError(CallErrorKind::NotCallable, Box::new(bindings)))
|
||||
Err(CallError(CallErrorKind::NotCallable, Box::new(self)))
|
||||
} else {
|
||||
Err(CallError(
|
||||
CallErrorKind::PossiblyNotCallable,
|
||||
Box::new(bindings),
|
||||
Box::new(self),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -98,6 +147,10 @@ impl<'db> Bindings<'db> {
|
||||
self.elements.len() == 1
|
||||
}
|
||||
|
||||
pub(crate) fn callable_type(&self) -> Type<'db> {
|
||||
self.signatures.callable_type
|
||||
}
|
||||
|
||||
/// Returns the return type of the call. For successful calls, this is the actual return type.
|
||||
/// For calls with binding errors, this is a type that best approximates the return type. For
|
||||
/// types that are not callable, returns `Type::Unknown`.
|
||||
@@ -122,12 +175,22 @@ impl<'db> Bindings<'db> {
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
self.callable_type.display(context.db())
|
||||
self.callable_type().display(context.db())
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (index, conflicting_form) in self.conflicting_forms.iter().enumerate() {
|
||||
if *conflicting_form {
|
||||
context.report_lint(
|
||||
&CONFLICTING_ARGUMENT_FORMS,
|
||||
BindingError::get_node(node, Some(index)),
|
||||
format_args!("Argument is used as both a value and a type form in call"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We currently only report errors for the first union element. Ideally, we'd report
|
||||
// an error saying that the union type can't be called, followed by subdiagnostics
|
||||
// explaining why.
|
||||
@@ -135,6 +198,286 @@ impl<'db> Bindings<'db> {
|
||||
first.report_diagnostics(context, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the return type of certain known callables, where we have special-case logic to
|
||||
/// determine the return type in a way that isn't directly expressible in the type system.
|
||||
fn evaluate_known_cases(&mut self, db: &'db dyn Db) {
|
||||
// Each special case listed here should have a corresponding clause in `Type::signatures`.
|
||||
for binding in &mut self.elements {
|
||||
let binding_type = binding.callable_type;
|
||||
let Some((overload_index, overload)) = binding.matching_overload_mut() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match binding_type {
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||
&& function.decorators(db).len() == 1
|
||||
{
|
||||
match overload.parameter_types() {
|
||||
[_, Some(owner)] => {
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, function, *owner,
|
||||
)),
|
||||
));
|
||||
}
|
||||
[Some(instance), None] => {
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
function,
|
||||
instance.to_meta_type(db),
|
||||
)),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if let [Some(first), _] = overload.parameter_types() {
|
||||
if first.is_none(db) {
|
||||
overload.set_return_type(Type::FunctionLiteral(function));
|
||||
} else {
|
||||
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
|
||||
BoundMethodType::new(db, function, *first),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
|
||||
if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||
&& function.decorators(db).len() == 1
|
||||
{
|
||||
match overload.parameter_types() {
|
||||
[_, _, Some(owner)] => {
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, *function, *owner,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(instance), None] => {
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
*function,
|
||||
instance.to_meta_type(db),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match overload.parameter_types() {
|
||||
[_, Some(instance), _] if instance.is_none(db) => {
|
||||
overload.set_return_type(*function_ty);
|
||||
}
|
||||
|
||||
[_, Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(
|
||||
type_alias,
|
||||
))), Some(Type::ClassLiteral(ClassLiteralType { class }))]
|
||||
if class.is_known(db, KnownClass::TypeAliasType)
|
||||
&& function.name(db) == "__name__" =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(
|
||||
db,
|
||||
type_alias.name(db),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), Some(Type::ClassLiteral(ClassLiteralType { class }))]
|
||||
if class.is_known(db, KnownClass::TypeVar)
|
||||
&& function.name(db) == "__name__" =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(
|
||||
db,
|
||||
typevar.name(db),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(_), _]
|
||||
if function
|
||||
.has_known_class_decorator(db, KnownClass::Property) =>
|
||||
{
|
||||
overload.set_return_type(todo_type!("@property"));
|
||||
}
|
||||
|
||||
[_, Some(instance), _] => {
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, *function, *instance,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(function_type) => match function_type.known(db) {
|
||||
Some(KnownFunction::IsEquivalentTo) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_equivalent_to(db, *ty_b),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsSubtypeOf) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_subtype_of(db, *ty_b),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsAssignableTo) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_assignable_to(db, *ty_b),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsDisjointFrom) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_disjoint_from(db, *ty_b),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsGradualEquivalentTo) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_gradual_equivalent_to(db, *ty_b),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsFullyStatic) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsSingleton) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsSingleValued) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::Len) => {
|
||||
if let [Some(first_arg)] = overload.parameter_types() {
|
||||
if let Some(len_ty) = first_arg.len(db) {
|
||||
overload.set_return_type(len_ty);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some(KnownFunction::Repr) => {
|
||||
if let [Some(first_arg)] = overload.parameter_types() {
|
||||
overload.set_return_type(first_arg.repr(db));
|
||||
};
|
||||
}
|
||||
|
||||
Some(KnownFunction::Cast) => {
|
||||
if let [Some(casted_ty), Some(_)] = overload.parameter_types() {
|
||||
overload.set_return_type(*casted_ty);
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::Overload) => {
|
||||
overload.set_return_type(todo_type!("overload(..) return type"));
|
||||
}
|
||||
|
||||
Some(KnownFunction::GetattrStatic) => {
|
||||
let [Some(instance_ty), Some(attr_name), default] =
|
||||
overload.parameter_types()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(attr_name) = attr_name.into_string_literal() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let default = if let Some(default) = default {
|
||||
*default
|
||||
} else {
|
||||
Type::Never
|
||||
};
|
||||
|
||||
let union_with_default = |ty| UnionType::from_elements(db, [ty, default]);
|
||||
|
||||
// TODO: we could emit a diagnostic here (if default is not set)
|
||||
overload.set_return_type(
|
||||
match instance_ty.static_member(db, attr_name.value(db)) {
|
||||
Symbol::Type(ty, Boundness::Bound) => {
|
||||
if instance_ty.is_fully_static(db) {
|
||||
ty
|
||||
} else {
|
||||
// Here, we attempt to model the fact that an attribute lookup on
|
||||
// a non-fully static type could fail. This is an approximation,
|
||||
// as there are gradual types like `tuple[Any]`, on which a lookup
|
||||
// of (e.g. of the `index` method) would always succeed.
|
||||
|
||||
union_with_default(ty)
|
||||
}
|
||||
}
|
||||
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
|
||||
union_with_default(ty)
|
||||
}
|
||||
Symbol::Unbound => default,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => match class.known(db) {
|
||||
Some(KnownClass::Bool) => match overload.parameter_types() {
|
||||
[Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)),
|
||||
[None] => overload.set_return_type(Type::BooleanLiteral(false)),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Some(KnownClass::Str) if overload_index == 0 => {
|
||||
match overload.parameter_types() {
|
||||
[Some(arg)] => overload.set_return_type(arg.str(db)),
|
||||
[None] => overload.set_return_type(Type::string_literal(db, "")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownClass::Type) if overload_index == 0 => {
|
||||
if let [Some(arg)] = overload.parameter_types() {
|
||||
overload.set_return_type(arg.to_meta_type(db));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// Not a special case
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a Bindings<'db> {
|
||||
@@ -170,7 +513,7 @@ impl<'a, 'db> IntoIterator for &'a mut Bindings<'db> {
|
||||
/// overloads, we store this error information for each overload.
|
||||
///
|
||||
/// [overloads]: https://github.com/python/typing/pull/1839
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CallableBinding<'db> {
|
||||
pub(crate) callable_type: Type<'db>,
|
||||
pub(crate) signature_type: Type<'db>,
|
||||
@@ -184,39 +527,55 @@ pub(crate) struct CallableBinding<'db> {
|
||||
}
|
||||
|
||||
impl<'db> CallableBinding<'db> {
|
||||
/// Bind a [`CallArguments`] against a [`CallableSignature`].
|
||||
///
|
||||
/// The returned [`CallableBinding`] provides the return type of the call, the bound types for
|
||||
/// all parameters, and any errors resulting from binding the call.
|
||||
fn bind(
|
||||
db: &'db dyn Db,
|
||||
fn match_parameters(
|
||||
signature: &CallableSignature<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
arguments: &mut CallArguments<'_>,
|
||||
argument_forms: &mut [Option<ParameterForm>],
|
||||
conflicting_forms: &mut [bool],
|
||||
) -> Self {
|
||||
// If this callable is a bound method, prepend the self instance onto the arguments list
|
||||
// before checking.
|
||||
let arguments = if let Some(bound_type) = signature.bound_type {
|
||||
Cow::Owned(arguments.with_self(bound_type))
|
||||
} else {
|
||||
Cow::Borrowed(arguments)
|
||||
};
|
||||
arguments.with_self(signature.bound_type, |arguments| {
|
||||
// TODO: This checks every overload. In the proposed more detailed call checking spec [1],
|
||||
// arguments are checked for arity first, and are only checked for type assignability against
|
||||
// the matching overloads. Make sure to implement that as part of separating call binding into
|
||||
// two phases.
|
||||
//
|
||||
// [1] https://github.com/python/typing/pull/1839
|
||||
let overloads = signature
|
||||
.into_iter()
|
||||
.map(|signature| {
|
||||
Binding::match_parameters(
|
||||
signature,
|
||||
arguments,
|
||||
argument_forms,
|
||||
conflicting_forms,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// TODO: This checks every overload. In the proposed more detailed call checking spec [1],
|
||||
// arguments are checked for arity first, and are only checked for type assignability against
|
||||
// the matching overloads. Make sure to implement that as part of separating call binding into
|
||||
// two phases.
|
||||
//
|
||||
// [1] https://github.com/python/typing/pull/1839
|
||||
let overloads = signature
|
||||
.into_iter()
|
||||
.map(|signature| Binding::bind(db, signature, arguments.as_ref()))
|
||||
.collect();
|
||||
CallableBinding {
|
||||
callable_type: signature.callable_type,
|
||||
signature_type: signature.signature_type,
|
||||
dunder_call_is_possibly_unbound: signature.dunder_call_is_possibly_unbound,
|
||||
overloads,
|
||||
}
|
||||
CallableBinding {
|
||||
callable_type: signature.callable_type,
|
||||
signature_type: signature.signature_type,
|
||||
dunder_call_is_possibly_unbound: signature.dunder_call_is_possibly_unbound,
|
||||
overloads,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn check_types(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
signature: &CallableSignature<'db>,
|
||||
argument_types: &mut CallArgumentTypes<'_, 'db>,
|
||||
) {
|
||||
// If this callable is a bound method, prepend the self instance onto the arguments list
|
||||
// before checking.
|
||||
argument_types.with_self(signature.bound_type, |argument_types| {
|
||||
for (signature, overload) in signature.iter().zip(&mut self.overloads) {
|
||||
overload.check_types(db, signature, argument_types);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn as_result(&self) -> Result<(), CallErrorKind> {
|
||||
@@ -333,27 +692,35 @@ impl<'db> CallableBinding<'db> {
|
||||
}
|
||||
|
||||
/// Binding information for one of the overloads of a callable.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Binding<'db> {
|
||||
/// Return type of the call.
|
||||
return_ty: Type<'db>,
|
||||
|
||||
/// Bound types for parameters, in parameter source order.
|
||||
parameter_tys: Box<[Type<'db>]>,
|
||||
/// The formal parameter that each argument is matched with, in argument source order, or
|
||||
/// `None` if the argument was not matched to any parameter.
|
||||
argument_parameters: Box<[Option<usize>]>,
|
||||
|
||||
/// Bound types for parameters, in parameter source order, or `None` if no argument was matched
|
||||
/// to that parameter.
|
||||
parameter_tys: Box<[Option<Type<'db>>]>,
|
||||
|
||||
/// Call binding errors, if any.
|
||||
errors: Vec<BindingError<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> Binding<'db> {
|
||||
fn bind(
|
||||
db: &'db dyn Db,
|
||||
fn match_parameters(
|
||||
signature: &Signature<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
arguments: &CallArguments<'_>,
|
||||
argument_forms: &mut [Option<ParameterForm>],
|
||||
conflicting_forms: &mut [bool],
|
||||
) -> Self {
|
||||
let parameters = signature.parameters();
|
||||
// The type assigned to each parameter at this call site.
|
||||
let mut parameter_tys = vec![None; parameters.len()];
|
||||
// The parameter that each argument is matched with.
|
||||
let mut argument_parameters = vec![None; arguments.len()];
|
||||
// Whether each parameter has been matched with an argument.
|
||||
let mut parameter_matched = vec![false; parameters.len()];
|
||||
let mut errors = vec![];
|
||||
let mut next_positional = 0;
|
||||
let mut first_excess_positional = None;
|
||||
@@ -370,9 +737,9 @@ impl<'db> Binding<'db> {
|
||||
}
|
||||
};
|
||||
for (argument_index, argument) in arguments.iter().enumerate() {
|
||||
let (index, parameter, argument_ty, positional) = match argument {
|
||||
Argument::Positional(ty) | Argument::Synthetic(ty) => {
|
||||
if matches!(argument, Argument::Synthetic(_)) {
|
||||
let (index, parameter, positional) = match argument {
|
||||
Argument::Positional | Argument::Synthetic => {
|
||||
if matches!(argument, Argument::Synthetic) {
|
||||
num_synthetic_args += 1;
|
||||
}
|
||||
let Some((index, parameter)) = parameters
|
||||
@@ -385,9 +752,9 @@ impl<'db> Binding<'db> {
|
||||
continue;
|
||||
};
|
||||
next_positional += 1;
|
||||
(index, parameter, ty, !parameter.is_variadic())
|
||||
(index, parameter, !parameter.is_variadic())
|
||||
}
|
||||
Argument::Keyword { name, ty } => {
|
||||
Argument::Keyword(name) => {
|
||||
let Some((index, parameter)) = parameters
|
||||
.keyword_by_name(name)
|
||||
.or_else(|| parameters.keyword_variadic())
|
||||
@@ -398,35 +765,33 @@ impl<'db> Binding<'db> {
|
||||
});
|
||||
continue;
|
||||
};
|
||||
(index, parameter, ty, false)
|
||||
(index, parameter, false)
|
||||
}
|
||||
|
||||
Argument::Variadic(_) | Argument::Keywords(_) => {
|
||||
Argument::Variadic | Argument::Keywords => {
|
||||
// TODO
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(expected_ty) = parameter.annotated_type() {
|
||||
if !argument_ty.is_assignable_to(db, expected_ty) {
|
||||
errors.push(BindingError::InvalidArgumentType {
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
expected_ty,
|
||||
provided_ty: *argument_ty,
|
||||
});
|
||||
if !matches!(argument, Argument::Synthetic) {
|
||||
if let Some(existing) =
|
||||
argument_forms[argument_index - num_synthetic_args].replace(parameter.form)
|
||||
{
|
||||
if existing != parameter.form {
|
||||
conflicting_forms[argument_index - num_synthetic_args] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
|
||||
if parameter.is_variadic() || parameter.is_keyword_variadic() {
|
||||
let union = UnionType::from_elements(db, [existing, *argument_ty]);
|
||||
parameter_tys[index].replace(union);
|
||||
} else {
|
||||
if parameter_matched[index] {
|
||||
if !parameter.is_variadic() && !parameter.is_keyword_variadic() {
|
||||
errors.push(BindingError::ParameterAlreadyAssigned {
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
});
|
||||
}
|
||||
}
|
||||
argument_parameters[argument_index] = Some(index);
|
||||
parameter_matched[index] = true;
|
||||
}
|
||||
if let Some(first_excess_argument_index) = first_excess_positional {
|
||||
errors.push(BindingError::TooManyPositionalArguments {
|
||||
@@ -439,8 +804,8 @@ impl<'db> Binding<'db> {
|
||||
});
|
||||
}
|
||||
let mut missing = vec![];
|
||||
for (index, bound_ty) in parameter_tys.iter().enumerate() {
|
||||
if bound_ty.is_none() {
|
||||
for (index, matched) in parameter_matched.iter().copied().enumerate() {
|
||||
if !matched {
|
||||
let param = ¶meters[index];
|
||||
if param.is_variadic()
|
||||
|| param.is_keyword_variadic()
|
||||
@@ -461,14 +826,65 @@ impl<'db> Binding<'db> {
|
||||
|
||||
Self {
|
||||
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
|
||||
parameter_tys: parameter_tys
|
||||
.into_iter()
|
||||
.map(|opt_ty| opt_ty.unwrap_or(Type::unknown()))
|
||||
.collect(),
|
||||
argument_parameters: argument_parameters.into_boxed_slice(),
|
||||
parameter_tys: vec![None; parameters.len()].into_boxed_slice(),
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_types(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
signature: &Signature<'db>,
|
||||
argument_types: &CallArgumentTypes<'_, 'db>,
|
||||
) {
|
||||
let parameters = signature.parameters();
|
||||
let mut num_synthetic_args = 0;
|
||||
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
|
||||
if argument_index >= num_synthetic_args {
|
||||
// Adjust the argument index to skip synthetic args, which don't appear at the call
|
||||
// site and thus won't be in the Call node arguments list.
|
||||
Some(argument_index - num_synthetic_args)
|
||||
} else {
|
||||
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
|
||||
// entire Call node, since there's no argument node for this argument at the call site
|
||||
None
|
||||
}
|
||||
};
|
||||
for (argument_index, (argument, argument_type)) in argument_types.iter().enumerate() {
|
||||
if matches!(argument, Argument::Synthetic) {
|
||||
num_synthetic_args += 1;
|
||||
}
|
||||
let Some(parameter_index) = self.argument_parameters[argument_index] else {
|
||||
// There was an error with argument when matching parameters, so don't bother
|
||||
// type-checking it.
|
||||
continue;
|
||||
};
|
||||
let parameter = ¶meters[parameter_index];
|
||||
if let Some(expected_ty) = parameter.annotated_type() {
|
||||
if !argument_type.is_assignable_to(db, expected_ty) {
|
||||
let positional = matches!(argument, Argument::Positional | Argument::Synthetic)
|
||||
&& !parameter.is_variadic();
|
||||
self.errors.push(BindingError::InvalidArgumentType {
|
||||
parameter: ParameterContext::new(parameter, parameter_index, positional),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
expected_ty,
|
||||
provided_ty: argument_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
// We still update the actual type of the parameter in this binding to match the
|
||||
// argument, even if the argument type is not assignable to the expected parameter
|
||||
// type.
|
||||
if let Some(existing) = self.parameter_tys[parameter_index].replace(argument_type) {
|
||||
// We already verified in `match_parameters` that we only match multiple arguments
|
||||
// with variadic parameters.
|
||||
let union = UnionType::from_elements(db, [existing, argument_type]);
|
||||
self.parameter_tys[parameter_index] = Some(union);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
|
||||
self.return_ty = return_ty;
|
||||
}
|
||||
@@ -477,7 +893,7 @@ impl<'db> Binding<'db> {
|
||||
self.return_ty
|
||||
}
|
||||
|
||||
pub(crate) fn parameter_types(&self) -> &[Type<'db>] {
|
||||
pub(crate) fn parameter_types(&self) -> &[Option<Type<'db>>] {
|
||||
&self.parameter_tys
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
||||
},
|
||||
types::{
|
||||
definition_expression_type, CallArguments, CallError, CallErrorKind, DynamicType,
|
||||
definition_expression_type, CallArgumentTypes, CallError, CallErrorKind, DynamicType,
|
||||
MetaclassCandidate, TupleType, UnionBuilder, UnionType,
|
||||
},
|
||||
Db, KnownModule, Program,
|
||||
@@ -128,9 +128,10 @@ impl<'db> Class<'db> {
|
||||
#[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)]
|
||||
fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
tracing::trace!("Class::explicit_bases_query: {}", self.name(db));
|
||||
let class_stmt = self.node(db);
|
||||
|
||||
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
|
||||
let class_stmt = self.node(db);
|
||||
let class_definition =
|
||||
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
|
||||
|
||||
class_stmt
|
||||
.bases()
|
||||
@@ -156,11 +157,15 @@ impl<'db> Class<'db> {
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
tracing::trace!("Class::decorators: {}", self.name(db));
|
||||
|
||||
let class_stmt = self.node(db);
|
||||
if class_stmt.decorator_list.is_empty() {
|
||||
return Box::new([]);
|
||||
}
|
||||
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
|
||||
|
||||
let class_definition =
|
||||
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
|
||||
|
||||
class_stmt
|
||||
.decorator_list
|
||||
.iter()
|
||||
@@ -224,9 +229,15 @@ impl<'db> Class<'db> {
|
||||
.as_ref()?
|
||||
.find_keyword("metaclass")?
|
||||
.value;
|
||||
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
|
||||
let metaclass_ty = definition_expression_type(db, class_definition, metaclass_node);
|
||||
Some(metaclass_ty)
|
||||
|
||||
let class_definition =
|
||||
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
|
||||
|
||||
Some(definition_expression_type(
|
||||
db,
|
||||
class_definition,
|
||||
metaclass_node,
|
||||
))
|
||||
}
|
||||
|
||||
/// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred.
|
||||
@@ -279,13 +290,13 @@ impl<'db> Class<'db> {
|
||||
let namespace = KnownClass::Dict.to_instance(db);
|
||||
|
||||
// TODO: Other keyword arguments?
|
||||
let arguments = CallArguments::positional([name, bases, namespace]);
|
||||
let arguments = CallArgumentTypes::positional([name, bases, namespace]);
|
||||
|
||||
let return_ty_result = match metaclass.try_call(db, &arguments) {
|
||||
let return_ty_result = match metaclass.try_call(db, arguments) {
|
||||
Ok(bindings) => Ok(bindings.return_type(db)),
|
||||
|
||||
Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::NotCallable(bindings.callable_type),
|
||||
kind: MetaclassErrorKind::NotCallable(bindings.callable_type()),
|
||||
}),
|
||||
|
||||
// TODO we should also check for binding errors that would indicate the metaclass
|
||||
@@ -834,6 +845,7 @@ pub enum KnownClass {
|
||||
TypeAliasType,
|
||||
NoDefaultType,
|
||||
NewType,
|
||||
Sized,
|
||||
// TODO: This can probably be removed when we have support for protocols
|
||||
SupportsIndex,
|
||||
// Collections
|
||||
@@ -911,6 +923,7 @@ impl<'db> KnownClass {
|
||||
| Self::DefaultDict
|
||||
| Self::Deque
|
||||
| Self::Float
|
||||
| Self::Sized
|
||||
| Self::Classmethod => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
@@ -955,6 +968,7 @@ impl<'db> KnownClass {
|
||||
Self::Counter => "Counter",
|
||||
Self::DefaultDict => "defaultdict",
|
||||
Self::Deque => "deque",
|
||||
Self::Sized => "Sized",
|
||||
Self::OrderedDict => "OrderedDict",
|
||||
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
|
||||
Self::StdlibAlias => "_Alias",
|
||||
@@ -1115,9 +1129,11 @@ impl<'db> KnownClass {
|
||||
| Self::MethodWrapperType
|
||||
| Self::WrapperDescriptorType => KnownModule::Types,
|
||||
Self::NoneType => KnownModule::Typeshed,
|
||||
Self::SpecialForm | Self::TypeVar | Self::StdlibAlias | Self::SupportsIndex => {
|
||||
KnownModule::Typing
|
||||
}
|
||||
Self::SpecialForm
|
||||
| Self::TypeVar
|
||||
| Self::StdlibAlias
|
||||
| Self::SupportsIndex
|
||||
| Self::Sized => KnownModule::Typing,
|
||||
Self::TypeAliasType | Self::TypeVarTuple | Self::ParamSpec | Self::NewType => {
|
||||
KnownModule::TypingExtensions
|
||||
}
|
||||
@@ -1195,6 +1211,7 @@ impl<'db> KnownClass {
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::NewType => false,
|
||||
}
|
||||
}
|
||||
@@ -1247,6 +1264,7 @@ impl<'db> KnownClass {
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::NewType => false,
|
||||
}
|
||||
}
|
||||
@@ -1299,6 +1317,7 @@ impl<'db> KnownClass {
|
||||
"_SpecialForm" => Self::SpecialForm,
|
||||
"_NoDefaultType" => Self::NoDefaultType,
|
||||
"SupportsIndex" => Self::SupportsIndex,
|
||||
"Sized" => Self::Sized,
|
||||
"_version_info" => Self::VersionInfo,
|
||||
"ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => {
|
||||
Self::EllipsisType
|
||||
@@ -1358,6 +1377,7 @@ impl<'db> KnownClass {
|
||||
| Self::SupportsIndex
|
||||
| Self::ParamSpec
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use ruff_db::{
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use super::{binding_type, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
use super::{binding_type, KnownFunction, Type, TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
@@ -177,9 +177,8 @@ impl<'db> InferContext<'db> {
|
||||
let mut function_scope_tys = index
|
||||
.ancestor_scopes(scope_id)
|
||||
.filter_map(|(_, scope)| scope.node().as_function())
|
||||
.filter_map(|function| {
|
||||
binding_type(self.db, index.definition(function)).into_function_literal()
|
||||
});
|
||||
.map(|node| binding_type(self.db, index.expect_single_definition(node)))
|
||||
.filter_map(Type::into_function_literal);
|
||||
|
||||
// Iterate over all functions and test if any is decorated with `@no_type_check`.
|
||||
function_scope_tys.any(|function_ty| {
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::sync::Arc;
|
||||
pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&CALL_NON_CALLABLE);
|
||||
registry.register_lint(&CALL_POSSIBLY_UNBOUND_METHOD);
|
||||
registry.register_lint(&CONFLICTING_ARGUMENT_FORMS);
|
||||
registry.register_lint(&CONFLICTING_DECLARATIONS);
|
||||
registry.register_lint(&CONFLICTING_METACLASS);
|
||||
registry.register_lint(&CYCLIC_CLASS_DEFINITION);
|
||||
@@ -106,6 +107,16 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks whether an argument is used as both a value and a type form in a call
|
||||
pub(crate) static CONFLICTING_ARGUMENT_FORMS = {
|
||||
summary: "detects when an argument is used as both a value and a type form in a call",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO #14889
|
||||
pub(crate) static CONFLICTING_DECLARATIONS = {
|
||||
|
||||
@@ -110,6 +110,9 @@ impl Display for DisplayRepresentation<'_> {
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
|
||||
f.write_str("<wrapper-descriptor `__get__` of `function` objects>")
|
||||
}
|
||||
Type::Callable(CallableType::SpecializedGetitem) => {
|
||||
f.write_str("<specialized `__getitem__`>")
|
||||
}
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
Type::IntLiteral(n) => n.fmt(f),
|
||||
@@ -292,7 +295,10 @@ impl Display for DisplayUnionType<'_> {
|
||||
db: self.db,
|
||||
});
|
||||
} else {
|
||||
join.entry(&element.display(self.db));
|
||||
join.entry(&DisplayMaybeParenthesizedType {
|
||||
ty: *element,
|
||||
db: self.db,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +410,28 @@ impl Display for DisplayMaybeNegatedType<'_> {
|
||||
if self.negated {
|
||||
f.write_str("~")?;
|
||||
}
|
||||
self.ty.display(self.db).fmt(f)
|
||||
DisplayMaybeParenthesizedType {
|
||||
ty: self.ty,
|
||||
db: self.db,
|
||||
}
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayMaybeParenthesizedType<'db> {
|
||||
ty: Type<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayMaybeParenthesizedType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Type::Callable(CallableType::General(_) | CallableType::MethodWrapperDunderGet(_)) =
|
||||
self.ty
|
||||
{
|
||||
write!(f, "({})", self.ty.display(self.db))
|
||||
} else {
|
||||
self.ty.display(self.db).fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,8 +503,7 @@ mod tests {
|
||||
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{
|
||||
KnownClass, Parameter, ParameterKind, Parameters, Signature, SliceLiteralType,
|
||||
StringLiteralType, Type,
|
||||
KnownClass, Parameter, Parameters, Signature, SliceLiteralType, StringLiteralType, Type,
|
||||
};
|
||||
use crate::Db;
|
||||
|
||||
@@ -574,13 +600,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
display_signature(
|
||||
&db,
|
||||
[Parameter::new(
|
||||
Some(Type::none(&db)),
|
||||
ParameterKind::PositionalOnly {
|
||||
name: None,
|
||||
default_ty: None
|
||||
}
|
||||
)],
|
||||
[Parameter::positional_only(None).with_annotated_type(Type::none(&db))],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
"(None, /) -> None"
|
||||
@@ -591,20 +611,11 @@ mod tests {
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: Name::new_static("x"),
|
||||
default_ty: Some(KnownClass::Int.to_instance(&db))
|
||||
}
|
||||
),
|
||||
Parameter::new(
|
||||
Some(KnownClass::Str.to_instance(&db)),
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: Name::new_static("y"),
|
||||
default_ty: Some(KnownClass::Str.to_instance(&db))
|
||||
}
|
||||
)
|
||||
Parameter::positional_or_keyword(Name::new_static("x"))
|
||||
.with_default_type(KnownClass::Int.to_instance(&db)),
|
||||
Parameter::positional_or_keyword(Name::new_static("y"))
|
||||
.with_annotated_type(KnownClass::Str.to_instance(&db))
|
||||
.with_default_type(KnownClass::Str.to_instance(&db)),
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
@@ -616,20 +627,8 @@ mod tests {
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("x")),
|
||||
default_ty: None
|
||||
}
|
||||
),
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("y")),
|
||||
default_ty: None
|
||||
}
|
||||
)
|
||||
Parameter::positional_only(Some(Name::new_static("x"))),
|
||||
Parameter::positional_only(Some(Name::new_static("y"))),
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
@@ -641,20 +640,8 @@ mod tests {
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("x")),
|
||||
default_ty: None
|
||||
}
|
||||
),
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: Name::new_static("y"),
|
||||
default_ty: None
|
||||
}
|
||||
)
|
||||
Parameter::positional_only(Some(Name::new_static("x"))),
|
||||
Parameter::positional_or_keyword(Name::new_static("y")),
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
@@ -666,20 +653,8 @@ mod tests {
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::KeywordOnly {
|
||||
name: Name::new_static("x"),
|
||||
default_ty: None
|
||||
}
|
||||
),
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::KeywordOnly {
|
||||
name: Name::new_static("y"),
|
||||
default_ty: None
|
||||
}
|
||||
)
|
||||
Parameter::keyword_only(Name::new_static("x")),
|
||||
Parameter::keyword_only(Name::new_static("y")),
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
@@ -691,20 +666,8 @@ mod tests {
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: Name::new_static("x"),
|
||||
default_ty: None
|
||||
}
|
||||
),
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::KeywordOnly {
|
||||
name: Name::new_static("y"),
|
||||
default_ty: None
|
||||
}
|
||||
)
|
||||
Parameter::positional_or_keyword(Name::new_static("x")),
|
||||
Parameter::keyword_only(Name::new_static("y")),
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
@@ -716,74 +679,28 @@ mod tests {
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("a")),
|
||||
default_ty: None
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(KnownClass::Int.to_instance(&db)),
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("b")),
|
||||
default_ty: None
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("c")),
|
||||
default_ty: Some(Type::IntLiteral(1)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(KnownClass::Int.to_instance(&db)),
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("d")),
|
||||
default_ty: Some(Type::IntLiteral(2)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: Name::new_static("e"),
|
||||
default_ty: Some(Type::IntLiteral(3)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(KnownClass::Int.to_instance(&db)),
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: Name::new_static("f"),
|
||||
default_ty: Some(Type::IntLiteral(4)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Type::object(&db)),
|
||||
ParameterKind::Variadic {
|
||||
name: Name::new_static("args")
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::KeywordOnly {
|
||||
name: Name::new_static("g"),
|
||||
default_ty: Some(Type::IntLiteral(5)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(KnownClass::Int.to_instance(&db)),
|
||||
ParameterKind::KeywordOnly {
|
||||
name: Name::new_static("h"),
|
||||
default_ty: Some(Type::IntLiteral(6)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(KnownClass::Str.to_instance(&db)),
|
||||
ParameterKind::KeywordVariadic {
|
||||
name: Name::new_static("kwargs")
|
||||
},
|
||||
),
|
||||
Parameter::positional_only(Some(Name::new_static("a"))),
|
||||
Parameter::positional_only(Some(Name::new_static("b")))
|
||||
.with_annotated_type(KnownClass::Int.to_instance(&db)),
|
||||
Parameter::positional_only(Some(Name::new_static("c")))
|
||||
.with_default_type(Type::IntLiteral(1)),
|
||||
Parameter::positional_only(Some(Name::new_static("d")))
|
||||
.with_annotated_type(KnownClass::Int.to_instance(&db))
|
||||
.with_default_type(Type::IntLiteral(2)),
|
||||
Parameter::positional_or_keyword(Name::new_static("e"))
|
||||
.with_default_type(Type::IntLiteral(3)),
|
||||
Parameter::positional_or_keyword(Name::new_static("f"))
|
||||
.with_annotated_type(KnownClass::Int.to_instance(&db))
|
||||
.with_default_type(Type::IntLiteral(4)),
|
||||
Parameter::variadic(Name::new_static("args"))
|
||||
.with_annotated_type(Type::object(&db)),
|
||||
Parameter::keyword_only(Name::new_static("g"))
|
||||
.with_default_type(Type::IntLiteral(5)),
|
||||
Parameter::keyword_only(Name::new_static("h"))
|
||||
.with_annotated_type(KnownClass::Int.to_instance(&db))
|
||||
.with_default_type(Type::IntLiteral(6)),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs"))
|
||||
.with_annotated_type(KnownClass::Str.to_instance(&db)),
|
||||
],
|
||||
Some(KnownClass::Bytes.to_instance(&db))
|
||||
),
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
//! the query cycle until a fixed-point is reached. Salsa has a built-in fixed limit on the number
|
||||
//! of iterations, so if we fail to converge, Salsa will eventually panic. (This should of course
|
||||
//! be considered a bug.)
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use itertools::{Either, Itertools};
|
||||
use ruff_db::diagnostic::{DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
@@ -45,8 +43,8 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use salsa;
|
||||
use salsa::plumbing::AsId;
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{file_to_module, resolve_module};
|
||||
use crate::module_name::{ModuleName, ModuleNameResolutionError};
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId};
|
||||
use crate::semantic_index::definition::{
|
||||
AssignmentDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey,
|
||||
@@ -54,14 +52,16 @@ use crate::semantic_index::definition::{
|
||||
};
|
||||
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::{FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId};
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind,
|
||||
};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::symbol::{
|
||||
builtins_module_scope, builtins_symbol, explicit_global_symbol,
|
||||
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
typing_extensions_symbol, Boundness, LookupError,
|
||||
};
|
||||
use crate::types::call::{Argument, CallArguments, CallError};
|
||||
use crate::types::call::{Argument, Bindings, CallArgumentTypes, CallArguments, CallError};
|
||||
use crate::types::diagnostic::{
|
||||
report_implicit_return_type, report_invalid_arguments_to_annotated,
|
||||
report_invalid_arguments_to_callable, report_invalid_assignment,
|
||||
@@ -79,12 +79,12 @@ use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType,
|
||||
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
|
||||
MetaclassCandidate, Parameter, Parameters, SliceLiteralType, SubclassOfType, Symbol,
|
||||
SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||
MetaclassCandidate, Parameter, ParameterForm, Parameters, SliceLiteralType, SubclassOfType,
|
||||
Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
|
||||
UnionType,
|
||||
};
|
||||
use crate::types::{CallableType, GeneralCallableType, ParameterKind, Signature};
|
||||
use crate::types::{CallableType, GeneralCallableType, Signature};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
@@ -102,7 +102,7 @@ use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use super::{CallDunderError, ParameterExpectation, ParameterExpectations};
|
||||
use super::CallDunderError;
|
||||
|
||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
||||
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||
@@ -491,7 +491,7 @@ enum DeclaredAndInferredType<'db> {
|
||||
/// [`TypeInferenceBuilder`] just for that definition, and we merge the returned [`TypeInference`]
|
||||
/// into the one we are currently building for the entire scope. Using the query in this way
|
||||
/// ensures that if we first infer types for some scattered definitions in a scope, and later for
|
||||
/// the entire scope, we don't re-infer any types, we re-use the cached inference for those
|
||||
/// the entire scope, we don't re-infer any types, we reuse the cached inference for those
|
||||
/// definitions and their sub-expressions.
|
||||
///
|
||||
/// Functions with a name like `infer_*_definition` take both a node and a [`Definition`], and are
|
||||
@@ -887,6 +887,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
definition,
|
||||
);
|
||||
}
|
||||
DefinitionKind::StarImport(import) => {
|
||||
self.infer_import_from_definition(import.import(), import.alias(), definition);
|
||||
}
|
||||
DefinitionKind::Assignment(assignment) => {
|
||||
self.infer_assignment_definition(assignment, definition);
|
||||
}
|
||||
@@ -1141,7 +1144,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_type_parameters(type_params);
|
||||
|
||||
if let Some(arguments) = class.arguments.as_deref() {
|
||||
self.infer_arguments(arguments, ParameterExpectations::default());
|
||||
let call_arguments = Self::parse_arguments(arguments);
|
||||
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
|
||||
self.infer_argument_types(arguments, call_arguments, &argument_forms);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1205,15 +1210,57 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
let is_overload = function.decorator_list.iter().any(|decorator| {
|
||||
|
||||
let is_overload_or_abstract = function.decorator_list.iter().any(|decorator| {
|
||||
let decorator_type = self.file_expression_type(&decorator.expression);
|
||||
|
||||
decorator_type
|
||||
.into_function_literal()
|
||||
.is_some_and(|f| f.is_known(self.db(), KnownFunction::Overload))
|
||||
match decorator_type {
|
||||
Type::FunctionLiteral(function) => matches!(
|
||||
function.known(self.db()),
|
||||
Some(KnownFunction::Overload | KnownFunction::AbstractMethod)
|
||||
),
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
// TODO: Protocol / abstract methods can have empty bodies
|
||||
if (self.in_stub() || is_overload)
|
||||
|
||||
let class_inherits_protocol_directly = (|| -> bool {
|
||||
let current_scope_id = self.scope().file_scope_id(self.db());
|
||||
let current_scope = self.index.scope(current_scope_id);
|
||||
let Some(parent_scope_id) = current_scope.parent() else {
|
||||
return false;
|
||||
};
|
||||
let parent_scope = self.index.scope(parent_scope_id);
|
||||
|
||||
let class_scope = match parent_scope.kind() {
|
||||
ScopeKind::Class => parent_scope,
|
||||
ScopeKind::Annotation => {
|
||||
let Some(class_scope_id) = parent_scope.parent() else {
|
||||
return false;
|
||||
};
|
||||
let potentially_class_scope = self.index.scope(class_scope_id);
|
||||
|
||||
match potentially_class_scope.kind() {
|
||||
ScopeKind::Class => potentially_class_scope,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let NodeWithScopeKind::Class(node_ref) = class_scope.node() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// TODO move this to `Class` once we add proper `Protocol` support
|
||||
node_ref.bases().iter().any(|base| {
|
||||
matches!(
|
||||
self.file_expression_type(base),
|
||||
Type::KnownInstance(KnownInstanceType::Protocol)
|
||||
)
|
||||
})
|
||||
})();
|
||||
|
||||
if (self.in_stub() || is_overload_or_abstract || class_inherits_protocol_directly)
|
||||
&& self.return_types_and_ranges.is_empty()
|
||||
&& is_stub_suite(&function.body)
|
||||
{
|
||||
@@ -1290,8 +1337,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_definition(&mut self, node: impl Into<DefinitionNodeKey>) {
|
||||
let definition = self.index.definition(node);
|
||||
fn infer_definition(&mut self, node: impl Into<DefinitionNodeKey> + std::fmt::Debug + Copy) {
|
||||
let definition = self.index.expect_single_definition(node);
|
||||
let result = infer_definition_types(self.db(), definition);
|
||||
self.extend(result);
|
||||
}
|
||||
@@ -1517,7 +1564,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) {
|
||||
if let Some(annotation) = parameter.annotation() {
|
||||
let _annotated_ty = self.file_expression_type(annotation);
|
||||
// TODO `tuple[annotated_ty, ...]`
|
||||
// TODO `tuple[annotated_type, ...]`
|
||||
let ty = KnownClass::Tuple.to_instance(self.db());
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
@@ -1548,7 +1595,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) {
|
||||
if let Some(annotation) = parameter.annotation() {
|
||||
let _annotated_ty = self.file_expression_type(annotation);
|
||||
// TODO `dict[str, annotated_ty]`
|
||||
// TODO `dict[str, annotated_type]`
|
||||
let ty = KnownClass::Dict.to_instance(self.db());
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
@@ -2276,7 +2323,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let successful_call = meta_dunder_set
|
||||
.try_call(
|
||||
db,
|
||||
&CallArguments::positional([meta_attr_ty, object_ty, value_ty]),
|
||||
CallArgumentTypes::positional([meta_attr_ty, object_ty, value_ty]),
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
@@ -2375,7 +2422,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let successful_call = meta_dunder_set
|
||||
.try_call(
|
||||
db,
|
||||
&CallArguments::positional([meta_attr_ty, object_ty, value_ty]),
|
||||
CallArgumentTypes::positional([
|
||||
meta_attr_ty,
|
||||
object_ty,
|
||||
value_ty,
|
||||
]),
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
@@ -2783,7 +2834,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let call = target_type.try_call_dunder(
|
||||
db,
|
||||
op.in_place_dunder(),
|
||||
&CallArguments::positional([value_type]),
|
||||
CallArgumentTypes::positional([value_type]),
|
||||
);
|
||||
|
||||
match call {
|
||||
@@ -2977,7 +3028,18 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} = import;
|
||||
|
||||
for alias in names {
|
||||
self.infer_definition(alias);
|
||||
let definitions = self.index.definitions(alias);
|
||||
if definitions.is_empty() {
|
||||
// If the module couldn't be resolved while constructing the semantic index,
|
||||
// this node won't have any definitions associated with it -- but we need to
|
||||
// make sure that we still emit the diagnostic for the unresolvable module,
|
||||
// since this will cause the import to fail at runtime.
|
||||
self.resolve_import_from_module(import, alias);
|
||||
} else {
|
||||
for definition in definitions {
|
||||
self.extend(infer_definition_types(self.db(), *definition));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3029,52 +3091,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a `from .foo import bar` relative import, resolve the relative module
|
||||
/// we're importing `bar` from into an absolute [`ModuleName`]
|
||||
/// using the name of the module we're currently analyzing.
|
||||
///
|
||||
/// - `level` is the number of dots at the beginning of the relative module name:
|
||||
/// - `from .foo.bar import baz` => `level == 1`
|
||||
/// - `from ...foo.bar import baz` => `level == 3`
|
||||
/// - `tail` is the relative module name stripped of all leading dots:
|
||||
/// - `from .foo import bar` => `tail == "foo"`
|
||||
/// - `from ..foo.bar import baz` => `tail == "foo.bar"`
|
||||
fn relative_module_name(
|
||||
&self,
|
||||
tail: Option<&str>,
|
||||
level: NonZeroU32,
|
||||
) -> Result<ModuleName, ModuleNameResolutionError> {
|
||||
let module = file_to_module(self.db(), self.file())
|
||||
.ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
|
||||
let mut level = level.get();
|
||||
|
||||
if module.kind().is_package() {
|
||||
level = level.saturating_sub(1);
|
||||
}
|
||||
|
||||
let mut module_name = module
|
||||
.name()
|
||||
.ancestors()
|
||||
.nth(level as usize)
|
||||
.ok_or(ModuleNameResolutionError::TooManyDots)?;
|
||||
|
||||
if let Some(tail) = tail {
|
||||
let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
|
||||
module_name.extend(&tail);
|
||||
}
|
||||
|
||||
Ok(module_name)
|
||||
}
|
||||
|
||||
fn infer_import_from_definition(
|
||||
/// Resolve the [`ModuleName`], and the type of the module, being referred to by an
|
||||
/// [`ast::StmtImportFrom`] node. Emit a diagnostic if the module cannot be resolved.
|
||||
fn resolve_import_from_module(
|
||||
&mut self,
|
||||
import_from: &'db ast::StmtImportFrom,
|
||||
import_from: &ast::StmtImportFrom,
|
||||
alias: &ast::Alias,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// TODO:
|
||||
// - Absolute `*` imports (`from collections import *`)
|
||||
// - Relative `*` imports (`from ...foo import *`)
|
||||
) -> Option<(ModuleName, Type<'db>)> {
|
||||
let ast::StmtImportFrom { module, level, .. } = import_from;
|
||||
// For diagnostics, we want to highlight the unresolvable
|
||||
// module and not the entire `from ... import ...` statement.
|
||||
@@ -3084,32 +3107,20 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.unwrap_or_else(|| AnyNodeRef::from(import_from));
|
||||
let module = module.as_deref();
|
||||
|
||||
let module_name = if let Some(level) = NonZeroU32::new(*level) {
|
||||
tracing::trace!(
|
||||
"Resolving imported object `{}` from module `{}` relative to file `{}`",
|
||||
alias.name,
|
||||
format_import_from_module(level.get(), module),
|
||||
self.file().path(self.db()),
|
||||
);
|
||||
self.relative_module_name(module, level)
|
||||
} else {
|
||||
tracing::trace!(
|
||||
"Resolving imported object `{}` from module `{}`",
|
||||
alias.name,
|
||||
format_import_from_module(*level, module),
|
||||
);
|
||||
module
|
||||
.and_then(ModuleName::new)
|
||||
.ok_or(ModuleNameResolutionError::InvalidSyntax)
|
||||
};
|
||||
tracing::trace!(
|
||||
"Resolving imported object `{}` from module `{}` into file `{}`",
|
||||
alias.name,
|
||||
format_import_from_module(*level, module),
|
||||
self.file().path(self.db()),
|
||||
);
|
||||
let module_name = ModuleName::from_import_statement(self.db(), self.file(), import_from);
|
||||
|
||||
let module_name = match module_name {
|
||||
Ok(module_name) => module_name,
|
||||
Err(ModuleNameResolutionError::InvalidSyntax) => {
|
||||
tracing::debug!("Failed to resolve import due to invalid syntax");
|
||||
// Invalid syntax diagnostics are emitted elsewhere.
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
Err(ModuleNameResolutionError::TooManyDots) => {
|
||||
tracing::debug!(
|
||||
@@ -3117,8 +3128,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
format_import_from_module(*level, module),
|
||||
);
|
||||
report_unresolved_module(&self.context, module_ref, *level, module);
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
Err(ModuleNameResolutionError::UnknownCurrentModule) => {
|
||||
tracing::debug!(
|
||||
@@ -3127,26 +3137,51 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.file().path(self.db())
|
||||
);
|
||||
report_unresolved_module(&self.context, module_ref, *level, module);
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(module_ty) = self.module_type_from_name(&module_name) else {
|
||||
report_unresolved_module(&self.context, module_ref, *level, module);
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((module_name, module_ty))
|
||||
}
|
||||
|
||||
fn infer_import_from_definition(
|
||||
&mut self,
|
||||
import_from: &'db ast::StmtImportFrom,
|
||||
alias: &ast::Alias,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
let Some((module_name, module_ty)) = self.resolve_import_from_module(import_from, alias)
|
||||
else {
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
return;
|
||||
};
|
||||
|
||||
let ast::Alias {
|
||||
range: _,
|
||||
name,
|
||||
asname: _,
|
||||
} = alias;
|
||||
// The indirection of having `star_import_info` as a separate variable
|
||||
// is required in order to make the borrow checker happy.
|
||||
let star_import_info = definition
|
||||
.kind(self.db())
|
||||
.as_star_import()
|
||||
.map(|star_import| {
|
||||
let symbol_table = self
|
||||
.index
|
||||
.symbol_table(self.scope().file_scope_id(self.db()));
|
||||
(star_import, symbol_table)
|
||||
});
|
||||
|
||||
let name = if let Some((star_import, symbol_table)) = star_import_info.as_ref() {
|
||||
symbol_table.symbol(star_import.symbol_id()).name()
|
||||
} else {
|
||||
&alias.name.id
|
||||
};
|
||||
|
||||
// First try loading the requested attribute from the module.
|
||||
if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), &name.id).symbol {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), name).symbol {
|
||||
if &alias.name != "*" && boundness == Boundness::PossiblyUnbound {
|
||||
// TODO: Consider loading _both_ the attribute and any submodule and unioning them
|
||||
// together if the attribute exists but is possibly-unbound.
|
||||
self.context.report_lint(
|
||||
@@ -3191,11 +3226,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
self.context.report_lint(
|
||||
&UNRESOLVED_IMPORT,
|
||||
AnyNodeRef::Alias(alias),
|
||||
format_args!("Module `{module_name}` has no member `{name}`",),
|
||||
);
|
||||
if &alias.name != "*" {
|
||||
self.context.report_lint(
|
||||
&UNRESOLVED_IMPORT,
|
||||
AnyNodeRef::Alias(alias),
|
||||
format_args!("Module `{module_name}` has no member `{name}`",),
|
||||
);
|
||||
}
|
||||
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
}
|
||||
|
||||
@@ -3232,45 +3270,22 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_expression(expression)
|
||||
}
|
||||
|
||||
fn infer_arguments<'a>(
|
||||
&mut self,
|
||||
arguments: &'a ast::Arguments,
|
||||
parameter_expectations: ParameterExpectations,
|
||||
) -> CallArguments<'a, 'db> {
|
||||
fn parse_arguments(arguments: &ast::Arguments) -> CallArguments<'_> {
|
||||
arguments
|
||||
.arguments_source_order()
|
||||
.enumerate()
|
||||
.map(|(index, arg_or_keyword)| {
|
||||
let infer_argument_type = match parameter_expectations.expectation_at_index(index) {
|
||||
ParameterExpectation::TypeExpression => Self::infer_type_expression,
|
||||
ParameterExpectation::ValueExpression => Self::infer_expression,
|
||||
};
|
||||
|
||||
.map(|arg_or_keyword| {
|
||||
match arg_or_keyword {
|
||||
ast::ArgOrKeyword::Arg(arg) => match arg {
|
||||
ast::Expr::Starred(ast::ExprStarred {
|
||||
value,
|
||||
range: _,
|
||||
ctx: _,
|
||||
}) => {
|
||||
let ty = infer_argument_type(self, value);
|
||||
self.store_expression_type(arg, ty);
|
||||
Argument::Variadic(ty)
|
||||
}
|
||||
ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic,
|
||||
// TODO diagnostic if after a keyword argument
|
||||
_ => Argument::Positional(infer_argument_type(self, arg)),
|
||||
_ => Argument::Positional,
|
||||
},
|
||||
ast::ArgOrKeyword::Keyword(ast::Keyword {
|
||||
arg,
|
||||
value,
|
||||
range: _,
|
||||
}) => {
|
||||
let ty = infer_argument_type(self, value);
|
||||
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => {
|
||||
if let Some(arg) = arg {
|
||||
Argument::Keyword { name: &arg.id, ty }
|
||||
Argument::Keyword(&arg.id)
|
||||
} else {
|
||||
// TODO diagnostic if not last
|
||||
Argument::Keywords(ty)
|
||||
Argument::Keywords
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3278,6 +3293,44 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn infer_argument_types<'a>(
|
||||
&mut self,
|
||||
ast_arguments: &ast::Arguments,
|
||||
arguments: CallArguments<'a>,
|
||||
argument_forms: &[Option<ParameterForm>],
|
||||
) -> CallArgumentTypes<'a, 'db> {
|
||||
let mut ast_arguments = ast_arguments.arguments_source_order();
|
||||
CallArgumentTypes::new(arguments, |index, _| {
|
||||
let arg_or_keyword = ast_arguments
|
||||
.next()
|
||||
.expect("argument lists should have consistent lengths");
|
||||
match arg_or_keyword {
|
||||
ast::ArgOrKeyword::Arg(arg) => match arg {
|
||||
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
|
||||
let ty = self.infer_argument_type(value, argument_forms[index]);
|
||||
self.store_expression_type(arg, ty);
|
||||
ty
|
||||
}
|
||||
_ => self.infer_argument_type(arg, argument_forms[index]),
|
||||
},
|
||||
ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => {
|
||||
self.infer_argument_type(value, argument_forms[index])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_argument_type(
|
||||
&mut self,
|
||||
ast_argument: &ast::Expr,
|
||||
form: Option<ParameterForm>,
|
||||
) -> Type<'db> {
|
||||
match form {
|
||||
None | Some(ParameterForm::Value) => self.infer_expression(ast_argument),
|
||||
Some(ParameterForm::Type) => self.infer_type_expression(ast_argument),
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_optional_expression(&mut self, expression: Option<&ast::Expr>) -> Option<Type<'db>> {
|
||||
expression.map(|expr| self.infer_expression(expr))
|
||||
}
|
||||
@@ -3701,7 +3754,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
fn infer_named_expression(&mut self, named: &ast::ExprNamed) -> Type<'db> {
|
||||
// See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements
|
||||
if named.target.is_name_expr() {
|
||||
let definition = self.index.definition(named);
|
||||
let definition = self.index.expect_single_definition(named);
|
||||
let result = infer_definition_types(self.db(), definition);
|
||||
self.extend(result);
|
||||
result.binding_type(definition)
|
||||
@@ -3769,64 +3822,44 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let positional_only = parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(parameter.name().id.clone()),
|
||||
default_ty: parameter
|
||||
.default()
|
||||
.map(|default| self.infer_expression(default)),
|
||||
},
|
||||
)
|
||||
.map(|param| {
|
||||
let mut parameter = Parameter::positional_only(Some(param.name().id.clone()));
|
||||
if let Some(default) = param.default() {
|
||||
parameter = parameter.with_default_type(self.infer_expression(default));
|
||||
}
|
||||
parameter
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let positional_or_keyword = parameters
|
||||
.args
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: parameter.name().id.clone(),
|
||||
default_ty: parameter
|
||||
.default()
|
||||
.map(|default| self.infer_expression(default)),
|
||||
},
|
||||
)
|
||||
.map(|param| {
|
||||
let mut parameter = Parameter::positional_or_keyword(param.name().id.clone());
|
||||
if let Some(default) = param.default() {
|
||||
parameter = parameter.with_default_type(self.infer_expression(default));
|
||||
}
|
||||
parameter
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let variadic = parameters.vararg.as_ref().map(|parameter| {
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::Variadic {
|
||||
name: parameter.name.id.clone(),
|
||||
},
|
||||
)
|
||||
});
|
||||
let variadic = parameters
|
||||
.vararg
|
||||
.as_ref()
|
||||
.map(|param| Parameter::variadic(param.name().id.clone()));
|
||||
let keyword_only = parameters
|
||||
.kwonlyargs
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::KeywordOnly {
|
||||
name: parameter.name().id.clone(),
|
||||
default_ty: parameter
|
||||
.default()
|
||||
.map(|default| self.infer_expression(default)),
|
||||
},
|
||||
)
|
||||
.map(|param| {
|
||||
let mut parameter = Parameter::keyword_only(param.name().id.clone());
|
||||
if let Some(default) = param.default() {
|
||||
parameter = parameter.with_default_type(self.infer_expression(default));
|
||||
}
|
||||
parameter
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let keyword_variadic = parameters.kwarg.as_ref().map(|parameter| {
|
||||
Parameter::new(
|
||||
None,
|
||||
ParameterKind::KeywordVariadic {
|
||||
name: parameter.name.id.clone(),
|
||||
},
|
||||
)
|
||||
});
|
||||
let keyword_variadic = parameters
|
||||
.kwarg
|
||||
.as_ref()
|
||||
.map(|param| Parameter::keyword_variadic(param.name().id.clone()));
|
||||
|
||||
Parameters::new(
|
||||
positional_only
|
||||
@@ -3856,16 +3889,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
arguments,
|
||||
} = call_expression;
|
||||
|
||||
// We don't call `Type::try_call`, because we want to perform type inference on the
|
||||
// arguments after matching them to parameters, but before checking that the argument types
|
||||
// are assignable to any parameter annotations.
|
||||
let mut call_arguments = Self::parse_arguments(arguments);
|
||||
let function_type = self.infer_expression(func);
|
||||
let signatures = function_type.signatures(self.db());
|
||||
let bindings = Bindings::match_parameters(signatures, &mut call_arguments);
|
||||
let mut call_argument_types =
|
||||
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
|
||||
|
||||
let parameter_expectations = function_type
|
||||
.into_function_literal()
|
||||
.and_then(|f| f.known(self.db()))
|
||||
.map(KnownFunction::parameter_expectations)
|
||||
.unwrap_or_default();
|
||||
|
||||
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
|
||||
match function_type.try_call(self.db(), &call_arguments) {
|
||||
match bindings.check_types(self.db(), &mut call_argument_types) {
|
||||
Ok(bindings) => {
|
||||
for binding in &bindings {
|
||||
let Some(known_function) = binding
|
||||
@@ -3882,7 +3916,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
match known_function {
|
||||
KnownFunction::RevealType => {
|
||||
if let [revealed_type] = overload.parameter_types() {
|
||||
if let [Some(revealed_type)] = overload.parameter_types() {
|
||||
self.context.report_diagnostic(
|
||||
call_expression,
|
||||
DiagnosticId::RevealedType,
|
||||
@@ -3896,7 +3930,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
KnownFunction::AssertType => {
|
||||
if let [actual_ty, asserted_ty] = overload.parameter_types() {
|
||||
if let [Some(actual_ty), Some(asserted_ty)] = overload.parameter_types()
|
||||
{
|
||||
if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) {
|
||||
self.context.report_lint(
|
||||
&TYPE_ASSERTION_FAILURE,
|
||||
@@ -3911,7 +3946,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
KnownFunction::StaticAssert => {
|
||||
if let [parameter_ty, message] = overload.parameter_types() {
|
||||
if let [Some(parameter_ty), message] = overload.parameter_types() {
|
||||
let truthiness = match parameter_ty.try_bool(self.db()) {
|
||||
Ok(truthiness) => truthiness,
|
||||
Err(err) => {
|
||||
@@ -3934,8 +3969,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
};
|
||||
|
||||
if !truthiness.is_always_true() {
|
||||
if let Some(message) =
|
||||
message.into_string_literal().map(|s| &**s.value(self.db()))
|
||||
if let Some(message) = message
|
||||
.and_then(Type::into_string_literal)
|
||||
.map(|s| &**s.value(self.db()))
|
||||
{
|
||||
self.context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
@@ -4352,7 +4388,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
match operand_type.try_call_dunder(
|
||||
self.db(),
|
||||
unary_dunder_method,
|
||||
&CallArguments::none(),
|
||||
CallArgumentTypes::none(),
|
||||
) {
|
||||
Ok(outcome) => outcome.return_type(self.db()),
|
||||
Err(e) => {
|
||||
@@ -4634,7 +4670,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
self.db(),
|
||||
reflected_dunder,
|
||||
&CallArguments::positional([left_ty]),
|
||||
CallArgumentTypes::positional([left_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.or_else(|_| {
|
||||
@@ -4642,7 +4678,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
self.db(),
|
||||
op.dunder(),
|
||||
&CallArguments::positional([right_ty]),
|
||||
CallArgumentTypes::positional([right_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
})
|
||||
@@ -4654,7 +4690,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
self.db(),
|
||||
op.dunder(),
|
||||
&CallArguments::positional([right_ty]),
|
||||
CallArgumentTypes::positional([right_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.ok();
|
||||
@@ -4667,7 +4703,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
self.db(),
|
||||
op.reflected_dunder(),
|
||||
&CallArguments::positional([left_ty]),
|
||||
CallArgumentTypes::positional([left_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.ok()
|
||||
@@ -5318,7 +5354,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
db,
|
||||
op.dunder(),
|
||||
&CallArguments::positional([Type::Instance(right)]),
|
||||
CallArgumentTypes::positional([Type::Instance(right)]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
.ok()
|
||||
@@ -5367,7 +5403,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
contains_dunder
|
||||
.try_call(
|
||||
db,
|
||||
&CallArguments::positional([Type::Instance(right), Type::Instance(left)]),
|
||||
CallArgumentTypes::positional([
|
||||
Type::Instance(right),
|
||||
Type::Instance(left),
|
||||
]),
|
||||
)
|
||||
.map(|bindings| bindings.return_type(db))
|
||||
.ok()
|
||||
@@ -5504,20 +5543,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
slice_ty: Type<'db>,
|
||||
) -> Type<'db> {
|
||||
match (value_ty, slice_ty) {
|
||||
(
|
||||
Type::Instance(instance),
|
||||
Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_),
|
||||
) if instance
|
||||
.class()
|
||||
.is_known(self.db(), KnownClass::VersionInfo) =>
|
||||
{
|
||||
self.infer_subscript_expression_types(
|
||||
value_node,
|
||||
Type::version_info_tuple(self.db()),
|
||||
slice_ty,
|
||||
)
|
||||
}
|
||||
|
||||
// Ex) Given `("a", "b", "c", "d")[1]`, return `"b"`
|
||||
(Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => {
|
||||
let elements = tuple_ty.elements(self.db());
|
||||
@@ -5643,7 +5668,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
match value_ty.try_call_dunder(
|
||||
self.db(),
|
||||
"__getitem__",
|
||||
&CallArguments::positional([slice_ty]),
|
||||
CallArgumentTypes::positional([slice_ty]),
|
||||
) {
|
||||
Ok(outcome) => return outcome.return_type(self.db()),
|
||||
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
|
||||
@@ -5659,12 +5684,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
return err.fallback_return_type(self.db());
|
||||
}
|
||||
Err(CallDunderError::CallError(_, bindings)) => {
|
||||
bindings.report_diagnostics(&self.context, value_node.into());
|
||||
self.context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node,
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
bindings.callable_type.display(self.db()),
|
||||
bindings.callable_type().display(self.db()),
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
@@ -5705,7 +5731,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
match ty.try_call(
|
||||
self.db(),
|
||||
&CallArguments::positional([value_ty, slice_ty]),
|
||||
CallArgumentTypes::positional([value_ty, slice_ty]),
|
||||
) {
|
||||
Ok(bindings) => return bindings.return_type(self.db()),
|
||||
Err(CallError(_, bindings)) => {
|
||||
@@ -5714,7 +5740,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
value_node,
|
||||
format_args!(
|
||||
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
bindings.callable_type.display(self.db()),
|
||||
bindings.callable_type().display(self.db()),
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
@@ -6974,13 +7000,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Parameters::todo()
|
||||
} else {
|
||||
Parameters::new(parameter_types.iter().map(|param_type| {
|
||||
Parameter::new(
|
||||
Some(*param_type),
|
||||
ParameterKind::PositionalOnly {
|
||||
name: None,
|
||||
default_ty: None,
|
||||
},
|
||||
)
|
||||
Parameter::positional_only(None).with_annotated_type(*param_type)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -7148,23 +7168,6 @@ fn format_import_from_module(level: u32, module: Option<&str>) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// Various ways in which resolving a [`ModuleName`]
|
||||
/// from an [`ast::StmtImport`] or [`ast::StmtImportFrom`] node might fail
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum ModuleNameResolutionError {
|
||||
/// The import statement has invalid syntax
|
||||
InvalidSyntax,
|
||||
|
||||
/// We couldn't resolve the file we're currently analyzing back to a module
|
||||
/// (Only necessary for relative import statements)
|
||||
UnknownCurrentModule,
|
||||
|
||||
/// The relative import statement seems to take us outside of the module search path
|
||||
/// (e.g. our current module is `foo.bar`, and the relative import statement in `foo.bar`
|
||||
/// is `from ....baz import spam`)
|
||||
TooManyDots,
|
||||
}
|
||||
|
||||
/// Struct collecting string parts when inferring a formatted string. Infers a string literal if the
|
||||
/// concatenated string is small enough, otherwise infers a literal string.
|
||||
///
|
||||
|
||||
@@ -103,12 +103,6 @@ mod stable {
|
||||
forall types s, t. s.is_subtype_of(db, t) && t.is_subtype_of(db, s) => s.is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// If `S <: T`, then `~T <: ~S`.
|
||||
type_property_test!(
|
||||
negation_reverses_subtype_order, db,
|
||||
forall types s, t. s.is_subtype_of(db, t) => t.negate(db).is_subtype_of(db, s.negate(db))
|
||||
);
|
||||
|
||||
// `T` is not disjoint from itself, unless `T` is `Never`.
|
||||
type_property_test!(
|
||||
disjoint_from_is_irreflexive, db,
|
||||
@@ -266,7 +260,7 @@ mod flaky {
|
||||
);
|
||||
|
||||
// Equal element sets of unions implies equivalence
|
||||
// flaky at laest in part because of https://github.com/astral-sh/ruff/issues/15513
|
||||
// flaky at least in part because of https://github.com/astral-sh/ruff/issues/15513
|
||||
type_property_test!(
|
||||
union_equivalence_not_order_dependent, db,
|
||||
forall types s, t, u.
|
||||
@@ -286,4 +280,19 @@ mod flaky {
|
||||
forall types s, t.
|
||||
!s.is_disjoint_from(db, union(db, [s, t])) && !t.is_disjoint_from(db, union(db, [s, t]))
|
||||
);
|
||||
|
||||
// If `S <: T`, then `~T <: ~S`.
|
||||
//
|
||||
// DO NOT STABILISE this test until the mdtests here pass:
|
||||
// https://github.com/astral-sh/ruff/blob/2711e08eb8eb38d1ce323aae0517fede371cba15/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md?plain=1#L276-L315
|
||||
//
|
||||
// This test has flakes relating to those subtyping and simplification tests
|
||||
// (see https://github.com/astral-sh/ruff/issues/16913), but it is hard to
|
||||
// reliably trigger the flakes when running this test manually as the flakes
|
||||
// occur very rarely (even running the test with several million seeds does
|
||||
// not always reliably reproduce the flake).
|
||||
type_property_test!(
|
||||
negation_reverses_subtype_order, db,
|
||||
forall types s, t. s.is_subtype_of(db, t) => t.negate(db).is_subtype_of(db, s.negate(db))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ impl<'db> Signatures<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> std::slice::Iter<'_, CallableSignature<'db>> {
|
||||
self.elements.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) {
|
||||
if self.callable_type == before {
|
||||
self.callable_type = after;
|
||||
@@ -87,7 +91,7 @@ impl<'a, 'db> IntoIterator for &'a Signatures<'db> {
|
||||
type IntoIter = std::slice::Iter<'a, CallableSignature<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.elements.iter()
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +183,10 @@ impl<'db> CallableSignature<'db> {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> std::slice::Iter<'_, Signature<'db>> {
|
||||
self.overloads.iter()
|
||||
}
|
||||
|
||||
fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) {
|
||||
if self.callable_type == before {
|
||||
self.callable_type = after;
|
||||
@@ -191,7 +199,7 @@ impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> {
|
||||
type IntoIter = std::slice::Iter<'a, Signature<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.overloads.iter()
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,18 +314,10 @@ impl<'db> Parameters<'db> {
|
||||
pub(crate) fn todo() -> Self {
|
||||
Self {
|
||||
value: vec![
|
||||
Parameter {
|
||||
annotated_ty: Some(todo_type!("todo signature *args")),
|
||||
kind: ParameterKind::Variadic {
|
||||
name: Name::new_static("args"),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(todo_type!("todo signature **kwargs")),
|
||||
kind: ParameterKind::KeywordVariadic {
|
||||
name: Name::new_static("kwargs"),
|
||||
},
|
||||
},
|
||||
Parameter::variadic(Name::new_static("args"))
|
||||
.with_annotated_type(todo_type!("todo signature *args")),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs"))
|
||||
.with_annotated_type(todo_type!("todo signature **kwargs")),
|
||||
],
|
||||
is_gradual: false,
|
||||
}
|
||||
@@ -331,18 +331,10 @@ impl<'db> Parameters<'db> {
|
||||
pub(crate) fn gradual_form() -> Self {
|
||||
Self {
|
||||
value: vec![
|
||||
Parameter {
|
||||
annotated_ty: Some(Type::Dynamic(DynamicType::Any)),
|
||||
kind: ParameterKind::Variadic {
|
||||
name: Name::new_static("args"),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(Type::Dynamic(DynamicType::Any)),
|
||||
kind: ParameterKind::KeywordVariadic {
|
||||
name: Name::new_static("kwargs"),
|
||||
},
|
||||
},
|
||||
Parameter::variadic(Name::new_static("args"))
|
||||
.with_annotated_type(Type::Dynamic(DynamicType::Any)),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs"))
|
||||
.with_annotated_type(Type::Dynamic(DynamicType::Any)),
|
||||
],
|
||||
is_gradual: true,
|
||||
}
|
||||
@@ -357,18 +349,10 @@ impl<'db> Parameters<'db> {
|
||||
pub(crate) fn unknown() -> Self {
|
||||
Self {
|
||||
value: vec![
|
||||
Parameter {
|
||||
annotated_ty: Some(Type::Dynamic(DynamicType::Unknown)),
|
||||
kind: ParameterKind::Variadic {
|
||||
name: Name::new_static("args"),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(Type::Dynamic(DynamicType::Unknown)),
|
||||
kind: ParameterKind::KeywordVariadic {
|
||||
name: Name::new_static("kwargs"),
|
||||
},
|
||||
},
|
||||
Parameter::variadic(Name::new_static("args"))
|
||||
.with_annotated_type(Type::Dynamic(DynamicType::Unknown)),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs"))
|
||||
.with_annotated_type(Type::Dynamic(DynamicType::Unknown)),
|
||||
],
|
||||
is_gradual: true,
|
||||
}
|
||||
@@ -387,7 +371,7 @@ impl<'db> Parameters<'db> {
|
||||
kwarg,
|
||||
range: _,
|
||||
} = parameters;
|
||||
let default_ty = |param: &ast::ParameterWithDefault| {
|
||||
let default_type = |param: &ast::ParameterWithDefault| {
|
||||
param
|
||||
.default()
|
||||
.map(|default| definition_expression_type(db, definition, default))
|
||||
@@ -399,7 +383,7 @@ impl<'db> Parameters<'db> {
|
||||
&arg.parameter,
|
||||
ParameterKind::PositionalOnly {
|
||||
name: Some(arg.parameter.name.id.clone()),
|
||||
default_ty: default_ty(arg),
|
||||
default_type: default_type(arg),
|
||||
},
|
||||
)
|
||||
});
|
||||
@@ -410,7 +394,7 @@ impl<'db> Parameters<'db> {
|
||||
&arg.parameter,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: arg.parameter.name.id.clone(),
|
||||
default_ty: default_ty(arg),
|
||||
default_type: default_type(arg),
|
||||
},
|
||||
)
|
||||
});
|
||||
@@ -431,7 +415,7 @@ impl<'db> Parameters<'db> {
|
||||
&arg.parameter,
|
||||
ParameterKind::KeywordOnly {
|
||||
name: arg.parameter.name.id.clone(),
|
||||
default_ty: default_ty(arg),
|
||||
default_type: default_type(arg),
|
||||
},
|
||||
)
|
||||
});
|
||||
@@ -531,14 +515,82 @@ impl<'db> std::ops::Index<usize> for Parameters<'db> {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub(crate) struct Parameter<'db> {
|
||||
/// Annotated type of the parameter.
|
||||
annotated_ty: Option<Type<'db>>,
|
||||
annotated_type: Option<Type<'db>>,
|
||||
|
||||
kind: ParameterKind<'db>,
|
||||
pub(crate) form: ParameterForm,
|
||||
}
|
||||
|
||||
impl<'db> Parameter<'db> {
|
||||
pub(crate) fn new(annotated_ty: Option<Type<'db>>, kind: ParameterKind<'db>) -> Self {
|
||||
Self { annotated_ty, kind }
|
||||
pub(crate) fn positional_only(name: Option<Name>) -> Self {
|
||||
Self {
|
||||
annotated_type: None,
|
||||
kind: ParameterKind::PositionalOnly {
|
||||
name,
|
||||
default_type: None,
|
||||
},
|
||||
form: ParameterForm::Value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn positional_or_keyword(name: Name) -> Self {
|
||||
Self {
|
||||
annotated_type: None,
|
||||
kind: ParameterKind::PositionalOrKeyword {
|
||||
name,
|
||||
default_type: None,
|
||||
},
|
||||
form: ParameterForm::Value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn variadic(name: Name) -> Self {
|
||||
Self {
|
||||
annotated_type: None,
|
||||
kind: ParameterKind::Variadic { name },
|
||||
form: ParameterForm::Value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn keyword_only(name: Name) -> Self {
|
||||
Self {
|
||||
annotated_type: None,
|
||||
kind: ParameterKind::KeywordOnly {
|
||||
name,
|
||||
default_type: None,
|
||||
},
|
||||
form: ParameterForm::Value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn keyword_variadic(name: Name) -> Self {
|
||||
Self {
|
||||
annotated_type: None,
|
||||
kind: ParameterKind::KeywordVariadic { name },
|
||||
form: ParameterForm::Value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_annotated_type(mut self, annotated_type: Type<'db>) -> Self {
|
||||
self.annotated_type = Some(annotated_type);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_default_type(mut self, default: Type<'db>) -> Self {
|
||||
match &mut self.kind {
|
||||
ParameterKind::PositionalOnly { default_type, .. }
|
||||
| ParameterKind::PositionalOrKeyword { default_type, .. }
|
||||
| ParameterKind::KeywordOnly { default_type, .. } => *default_type = Some(default),
|
||||
ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => {
|
||||
panic!("cannot set default value for variadic parameter")
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn type_form(mut self) -> Self {
|
||||
self.form = ParameterForm::Type;
|
||||
self
|
||||
}
|
||||
|
||||
fn from_node_and_kind(
|
||||
@@ -548,10 +600,11 @@ impl<'db> Parameter<'db> {
|
||||
kind: ParameterKind<'db>,
|
||||
) -> Self {
|
||||
Self {
|
||||
annotated_ty: parameter
|
||||
annotated_type: parameter
|
||||
.annotation()
|
||||
.map(|annotation| definition_expression_type(db, definition, annotation)),
|
||||
kind,
|
||||
form: ParameterForm::Value,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,7 +651,7 @@ impl<'db> Parameter<'db> {
|
||||
|
||||
/// Annotated type of the parameter, if annotated.
|
||||
pub(crate) fn annotated_type(&self) -> Option<Type<'db>> {
|
||||
self.annotated_ty
|
||||
self.annotated_type
|
||||
}
|
||||
|
||||
/// Kind of the parameter.
|
||||
@@ -629,11 +682,10 @@ impl<'db> Parameter<'db> {
|
||||
/// Default-value type of the parameter, if any.
|
||||
pub(crate) fn default_type(&self) -> Option<Type<'db>> {
|
||||
match self.kind {
|
||||
ParameterKind::PositionalOnly { default_ty, .. } => default_ty,
|
||||
ParameterKind::PositionalOrKeyword { default_ty, .. } => default_ty,
|
||||
ParameterKind::Variadic { .. } => None,
|
||||
ParameterKind::KeywordOnly { default_ty, .. } => default_ty,
|
||||
ParameterKind::KeywordVariadic { .. } => None,
|
||||
ParameterKind::PositionalOnly { default_type, .. }
|
||||
| ParameterKind::PositionalOrKeyword { default_type, .. }
|
||||
| ParameterKind::KeywordOnly { default_type, .. } => default_type,
|
||||
ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -647,14 +699,14 @@ pub(crate) enum ParameterKind<'db> {
|
||||
/// It is possible for signatures to be defined in ways that leave positional-only parameters
|
||||
/// nameless (e.g. via `Callable` annotations).
|
||||
name: Option<Name>,
|
||||
default_ty: Option<Type<'db>>,
|
||||
default_type: Option<Type<'db>>,
|
||||
},
|
||||
|
||||
/// Positional-or-keyword parameter, e.g. `def f(x): ...`
|
||||
PositionalOrKeyword {
|
||||
/// Parameter name.
|
||||
name: Name,
|
||||
default_ty: Option<Type<'db>>,
|
||||
default_type: Option<Type<'db>>,
|
||||
},
|
||||
|
||||
/// Variadic parameter, e.g. `def f(*args): ...`
|
||||
@@ -667,7 +719,7 @@ pub(crate) enum ParameterKind<'db> {
|
||||
KeywordOnly {
|
||||
/// Parameter name.
|
||||
name: Name,
|
||||
default_ty: Option<Type<'db>>,
|
||||
default_type: Option<Type<'db>>,
|
||||
},
|
||||
|
||||
/// Variadic keywords parameter, e.g. `def f(**kwargs): ...`
|
||||
@@ -677,6 +729,13 @@ pub(crate) enum ParameterKind<'db> {
|
||||
},
|
||||
}
|
||||
|
||||
/// Whether a parameter is used as a value or a type form.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub(crate) enum ParameterForm {
|
||||
Value,
|
||||
Type,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -734,74 +793,28 @@ mod tests {
|
||||
assert_params(
|
||||
&sig,
|
||||
&[
|
||||
Parameter {
|
||||
annotated_ty: None,
|
||||
kind: ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("a")),
|
||||
default_ty: None,
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
|
||||
kind: ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("b")),
|
||||
default_ty: None,
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: None,
|
||||
kind: ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("c")),
|
||||
default_ty: Some(Type::IntLiteral(1)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
|
||||
kind: ParameterKind::PositionalOnly {
|
||||
name: Some(Name::new_static("d")),
|
||||
default_ty: Some(Type::IntLiteral(2)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: None,
|
||||
kind: ParameterKind::PositionalOrKeyword {
|
||||
name: Name::new_static("e"),
|
||||
default_ty: Some(Type::IntLiteral(3)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(Type::IntLiteral(4)),
|
||||
kind: ParameterKind::PositionalOrKeyword {
|
||||
name: Name::new_static("f"),
|
||||
default_ty: Some(Type::IntLiteral(4)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(Type::object(&db)),
|
||||
kind: ParameterKind::Variadic {
|
||||
name: Name::new_static("args"),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: None,
|
||||
kind: ParameterKind::KeywordOnly {
|
||||
name: Name::new_static("g"),
|
||||
default_ty: Some(Type::IntLiteral(5)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(Type::IntLiteral(6)),
|
||||
kind: ParameterKind::KeywordOnly {
|
||||
name: Name::new_static("h"),
|
||||
default_ty: Some(Type::IntLiteral(6)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
annotated_ty: Some(KnownClass::Str.to_instance(&db)),
|
||||
kind: ParameterKind::KeywordVariadic {
|
||||
name: Name::new_static("kwargs"),
|
||||
},
|
||||
},
|
||||
Parameter::positional_only(Some(Name::new_static("a"))),
|
||||
Parameter::positional_only(Some(Name::new_static("b")))
|
||||
.with_annotated_type(KnownClass::Int.to_instance(&db)),
|
||||
Parameter::positional_only(Some(Name::new_static("c")))
|
||||
.with_default_type(Type::IntLiteral(1)),
|
||||
Parameter::positional_only(Some(Name::new_static("d")))
|
||||
.with_annotated_type(KnownClass::Int.to_instance(&db))
|
||||
.with_default_type(Type::IntLiteral(2)),
|
||||
Parameter::positional_or_keyword(Name::new_static("e"))
|
||||
.with_default_type(Type::IntLiteral(3)),
|
||||
Parameter::positional_or_keyword(Name::new_static("f"))
|
||||
.with_annotated_type(Type::IntLiteral(4))
|
||||
.with_default_type(Type::IntLiteral(4)),
|
||||
Parameter::variadic(Name::new_static("args"))
|
||||
.with_annotated_type(Type::object(&db)),
|
||||
Parameter::keyword_only(Name::new_static("g"))
|
||||
.with_default_type(Type::IntLiteral(5)),
|
||||
Parameter::keyword_only(Name::new_static("h"))
|
||||
.with_annotated_type(Type::IntLiteral(6))
|
||||
.with_default_type(Type::IntLiteral(6)),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs"))
|
||||
.with_annotated_type(KnownClass::Str.to_instance(&db)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -828,15 +841,16 @@ mod tests {
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
let [Parameter {
|
||||
annotated_ty,
|
||||
annotated_type,
|
||||
kind: ParameterKind::PositionalOrKeyword { name, .. },
|
||||
..
|
||||
}] = &sig.parameters.value[..]
|
||||
else {
|
||||
panic!("expected one positional-or-keyword parameter");
|
||||
};
|
||||
assert_eq!(name, "a");
|
||||
// Parameter resolution not deferred; we should see A not B
|
||||
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "A");
|
||||
assert_eq!(annotated_type.unwrap().display(&db).to_string(), "A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -861,15 +875,16 @@ mod tests {
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
let [Parameter {
|
||||
annotated_ty,
|
||||
annotated_type,
|
||||
kind: ParameterKind::PositionalOrKeyword { name, .. },
|
||||
..
|
||||
}] = &sig.parameters.value[..]
|
||||
else {
|
||||
panic!("expected one positional-or-keyword parameter");
|
||||
};
|
||||
assert_eq!(name, "a");
|
||||
// Parameter resolution deferred; we should see B
|
||||
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "B");
|
||||
assert_eq!(annotated_type.unwrap().display(&db).to_string(), "B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -894,11 +909,13 @@ mod tests {
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
let [Parameter {
|
||||
annotated_ty: a_annotated_ty,
|
||||
annotated_type: a_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { name: a_name, .. },
|
||||
..
|
||||
}, Parameter {
|
||||
annotated_ty: b_annotated_ty,
|
||||
annotated_type: b_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { name: b_name, .. },
|
||||
..
|
||||
}] = &sig.parameters.value[..]
|
||||
else {
|
||||
panic!("expected two positional-or-keyword parameters");
|
||||
@@ -935,11 +952,13 @@ mod tests {
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
let [Parameter {
|
||||
annotated_ty: a_annotated_ty,
|
||||
annotated_type: a_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { name: a_name, .. },
|
||||
..
|
||||
}, Parameter {
|
||||
annotated_ty: b_annotated_ty,
|
||||
annotated_type: b_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { name: b_name, .. },
|
||||
..
|
||||
}] = &sig.parameters.value[..]
|
||||
else {
|
||||
panic!("expected two positional-or-keyword parameters");
|
||||
|
||||
@@ -83,6 +83,13 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
|
||||
|
||||
(
|
||||
Type::Callable(CallableType::SpecializedGetitem),
|
||||
Type::Callable(CallableType::SpecializedGetitem),
|
||||
) => Ordering::Equal,
|
||||
(Type::Callable(CallableType::SpecializedGetitem), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::SpecializedGetitem)) => Ordering::Greater,
|
||||
|
||||
(Type::Callable(CallableType::General(_)), Type::Callable(CallableType::General(_))) => {
|
||||
Ordering::Equal
|
||||
}
|
||||
|
||||
@@ -617,8 +617,9 @@ impl<'s> Parser<'s> {
|
||||
.extension()
|
||||
.is_none_or(|extension| extension.eq_ignore_ascii_case(expected_extension))
|
||||
{
|
||||
let backtick_start = self.line_index(backtick_offsets.0);
|
||||
bail!(
|
||||
"File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block"
|
||||
"File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block at line `{backtick_start}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -750,6 +751,10 @@ impl<'s> Parser<'s> {
|
||||
fn offset(&self) -> TextSize {
|
||||
self.source_len - self.cursor.text_len()
|
||||
}
|
||||
|
||||
fn line_index(&self, char_index: TextSize) -> u32 {
|
||||
self.source.count_lines(TextRange::up_to(char_index))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1221,7 +1226,7 @@ mod tests {
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block"
|
||||
"File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block at line `5`"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class MIMEPart(Message[_HeaderRegistryT, _HeaderRegistryParamT]):
|
||||
def attach(self, payload: Self) -> None: ... # type: ignore[override]
|
||||
# The attachments are created via type(self) in the attach method. It's theoretically
|
||||
# possible to sneak other attachment types into a MIMEPart instance, but could cause
|
||||
# cause unforseen consequences.
|
||||
# cause unforeseen consequences.
|
||||
def iter_attachments(self) -> Iterator[Self]: ...
|
||||
def iter_parts(self) -> Iterator[MIMEPart[_HeaderRegistryT]]: ...
|
||||
def get_content(self, *args: Any, content_manager: ContentManager | None = None, **kw: Any) -> Any: ...
|
||||
|
||||
@@ -20,10 +20,10 @@ default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
red_knot_project = { workspace = true, default-features = false, features = ["deflate"] }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
|
||||
ruff_db = { workspace = true, default-features = false, features = [] }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
@@ -34,6 +34,7 @@ log = { workspace = true }
|
||||
# Not a direct dependency but required to enable the `wasm_js` feature.
|
||||
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
|
||||
getrandom = { workspace = true, features = ["wasm_js"] }
|
||||
serde-wasm-bindgen = { workspace = true }
|
||||
|
||||
wasm-bindgen = { workspace = true }
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::any::Any;
|
||||
|
||||
use js_sys::{Error, JsString};
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::value::RangedValue;
|
||||
use red_knot_project::metadata::options::Options;
|
||||
use red_knot_project::metadata::value::ValueSource;
|
||||
use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
|
||||
use red_knot_project::ProjectMetadata;
|
||||
use red_knot_project::{Db, ProjectDatabase};
|
||||
use red_knot_python_semantic::Program;
|
||||
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
@@ -39,62 +40,75 @@ pub fn run() {
|
||||
pub struct Workspace {
|
||||
db: ProjectDatabase,
|
||||
system: WasmSystem,
|
||||
options: Options,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Workspace {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(root: &str, settings: &Settings) -> Result<Workspace, Error> {
|
||||
pub fn new(root: &str, options: JsValue) -> Result<Workspace, Error> {
|
||||
let options = Options::deserialize_with(
|
||||
ValueSource::Cli,
|
||||
serde_wasm_bindgen::Deserializer::from(options),
|
||||
)
|
||||
.map_err(into_error)?;
|
||||
|
||||
let system = WasmSystem::new(SystemPath::new(root));
|
||||
|
||||
let mut workspace =
|
||||
ProjectMetadata::discover(SystemPath::new(root), &system).map_err(into_error)?;
|
||||
let project = ProjectMetadata::from_options(options, SystemPathBuf::from(root), None)
|
||||
.map_err(into_error)?;
|
||||
|
||||
let options = Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(settings.python_version.into())),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
};
|
||||
let db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?;
|
||||
|
||||
workspace.apply_cli_options(options.clone());
|
||||
Ok(Self { db, system })
|
||||
}
|
||||
|
||||
let db = ProjectDatabase::new(workspace, system.clone()).map_err(into_error)?;
|
||||
#[wasm_bindgen(js_name = "updateOptions")]
|
||||
pub fn update_options(&mut self, options: JsValue) -> Result<(), Error> {
|
||||
let options = Options::deserialize_with(
|
||||
ValueSource::Cli,
|
||||
serde_wasm_bindgen::Deserializer::from(options),
|
||||
)
|
||||
.map_err(into_error)?;
|
||||
|
||||
Ok(Self {
|
||||
db,
|
||||
system,
|
||||
let project = ProjectMetadata::from_options(
|
||||
options,
|
||||
})
|
||||
self.db.project().root(&self.db).to_path_buf(),
|
||||
None,
|
||||
)
|
||||
.map_err(into_error)?;
|
||||
|
||||
let program_settings = project.to_program_settings(&self.system);
|
||||
Program::get(&self.db)
|
||||
.update_from_settings(&mut self.db, program_settings)
|
||||
.map_err(into_error)?;
|
||||
|
||||
self.db.project().reload(&mut self.db, project);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "openFile")]
|
||||
pub fn open_file(&mut self, path: &str, contents: &str) -> Result<FileHandle, Error> {
|
||||
let path = SystemPath::new(path);
|
||||
let path = SystemPath::absolute(path, self.db.project().root(&self.db));
|
||||
|
||||
self.system
|
||||
.fs
|
||||
.write_file_all(path, contents)
|
||||
.write_file_all(&path, contents)
|
||||
.map_err(into_error)?;
|
||||
|
||||
self.db.apply_changes(
|
||||
vec![ChangeEvent::Created {
|
||||
path: path.to_path_buf(),
|
||||
path: path.clone(),
|
||||
kind: CreatedKind::File,
|
||||
}],
|
||||
Some(&self.options),
|
||||
None,
|
||||
);
|
||||
|
||||
let file = system_path_to_file(&self.db, path).expect("File to exist");
|
||||
let file = system_path_to_file(&self.db, &path).expect("File to exist");
|
||||
|
||||
self.db.project().open_file(&mut self.db, file);
|
||||
|
||||
Ok(FileHandle {
|
||||
file,
|
||||
path: path.to_path_buf(),
|
||||
})
|
||||
Ok(FileHandle { path, file })
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "updateFile")]
|
||||
@@ -119,7 +133,7 @@ impl Workspace {
|
||||
kind: ChangedKind::FileMetadata,
|
||||
},
|
||||
],
|
||||
Some(&self.options),
|
||||
None,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -140,7 +154,7 @@ impl Workspace {
|
||||
path: file_id.path.to_path_buf(),
|
||||
kind: DeletedKind::File,
|
||||
}],
|
||||
Some(&self.options),
|
||||
None,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -202,18 +216,6 @@ impl FileHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Settings {
|
||||
pub python_version: PythonVersion,
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
impl Settings {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(python_version: PythonVersion) -> Self {
|
||||
Self { python_version }
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Diagnostic {
|
||||
#[wasm_bindgen(readonly)]
|
||||
@@ -332,33 +334,6 @@ impl From<ruff_text_size::TextRange> for TextRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
pub enum PythonVersion {
|
||||
Py37,
|
||||
Py38,
|
||||
#[default]
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
Py312,
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl From<PythonVersion> for ruff_python_ast::PythonVersion {
|
||||
fn from(value: PythonVersion) -> Self {
|
||||
match value {
|
||||
PythonVersion::Py37 => Self::PY37,
|
||||
PythonVersion::Py38 => Self::PY38,
|
||||
PythonVersion::Py39 => Self::PY39,
|
||||
PythonVersion::Py310 => Self::PY310,
|
||||
PythonVersion::Py311 => Self::PY311,
|
||||
PythonVersion::Py312 => Self::PY312,
|
||||
PythonVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct WasmSystem {
|
||||
fs: MemoryFileSystem,
|
||||
@@ -455,16 +430,3 @@ impl System for WasmSystem {
|
||||
fn not_found() -> std::io::Error {
|
||||
std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::PythonVersion;
|
||||
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
ruff_python_ast::PythonVersion::from(PythonVersion::default()),
|
||||
ruff_python_ast::PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use red_knot_wasm::{Position, Workspace};
|
||||
use wasm_bindgen_test::wasm_bindgen_test;
|
||||
|
||||
use red_knot_wasm::{Position, PythonVersion, Settings, Workspace};
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn check() {
|
||||
let settings = Settings {
|
||||
python_version: PythonVersion::Py312,
|
||||
};
|
||||
let mut workspace = Workspace::new("/", &settings).expect("Workspace to be created");
|
||||
let mut workspace =
|
||||
Workspace::new("/", js_sys::JSON::parse("{}").unwrap()).expect("Workspace to be created");
|
||||
|
||||
workspace
|
||||
.open_file("test.py", "import random22\n")
|
||||
|
||||
@@ -1054,6 +1054,52 @@ def mvce(keys, values):
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_statistics_syntax_errors() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--statistics", "--target-version=py39", "--preview"])
|
||||
.build();
|
||||
|
||||
// ParseError
|
||||
assert_cmd_snapshot!(
|
||||
cmd.pass_stdin("x ="),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
1 syntax-error
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// match before 3.10, UnsupportedSyntaxError
|
||||
assert_cmd_snapshot!(
|
||||
cmd.pass_stdin("match 2:\n case 1: ..."),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
1 syntax-error
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// rebound comprehension variable, SemanticSyntaxError
|
||||
assert_cmd_snapshot!(
|
||||
cmd.pass_stdin("[x := 1 for x in range(0)]"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
1 syntax-error
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_enabled_prefix() {
|
||||
// All the RUF9XX test rules should be triggered
|
||||
|
||||
@@ -5491,3 +5491,68 @@ fn cookiecutter_globbing_no_project_root() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that semantic syntax errors (1) are emitted, (2) are not cached, (3) don't affect the
|
||||
/// reporting of normal diagnostics, and (4) are not suppressed by `select = []` (or otherwise
|
||||
/// disabling all AST-based rules).
|
||||
#[test]
|
||||
fn semantic_syntax_errors() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let contents = "[(x := 1) for x in foo]";
|
||||
fs::write(tempdir.path().join("main.py"), contents)?;
|
||||
|
||||
let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
|
||||
// inline STDIN_BASE_OPTIONS to remove --no-cache
|
||||
cmd.args(["check", "--output-format", "concise"])
|
||||
.arg("--preview")
|
||||
.arg("--quiet") // suppress `debug build without --no-cache` warnings
|
||||
.current_dir(&tempdir);
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
cmd,
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
main.py:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
|
||||
main.py:1:20: F821 Undefined name `foo`
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
// this should *not* be cached, like normal parse errors
|
||||
assert_cmd_snapshot!(
|
||||
cmd,
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
main.py:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
|
||||
main.py:1:20: F821 Undefined name `foo`
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
// ensure semantic errors are caught even without AST-based rules selected
|
||||
assert_cmd_snapshot!(
|
||||
Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", "lint.select = []"])
|
||||
.arg("--preview")
|
||||
.arg("-")
|
||||
.pass_stdin(contents),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -937,7 +937,7 @@ impl From<snippet::Level> for DisplayAnnotationType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Information whether the header is the initial one or a consequitive one
|
||||
/// Information whether the header is the initial one or a consecutive one
|
||||
/// for multi-slice cases.
|
||||
// TODO: private
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
||||
@@ -60,23 +60,13 @@ type KeyDiagnosticFields = (
|
||||
Severity,
|
||||
);
|
||||
|
||||
static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[
|
||||
// We don't support `*` imports yet:
|
||||
(
|
||||
DiagnosticId::lint("unresolved-import"),
|
||||
Some("/src/tomllib/_parser.py"),
|
||||
Some(192..200),
|
||||
Cow::Borrowed("Module `collections.abc` has no member `Iterable`"),
|
||||
Severity::Error,
|
||||
),
|
||||
(
|
||||
DiagnosticId::lint("unused-ignore-comment"),
|
||||
Some("/src/tomllib/_parser.py"),
|
||||
Some(22299..22333),
|
||||
Cow::Borrowed("Unused blanket `type: ignore` directive"),
|
||||
Severity::Warning,
|
||||
),
|
||||
];
|
||||
static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[(
|
||||
DiagnosticId::lint("unused-ignore-comment"),
|
||||
Some("/src/tomllib/_parser.py"),
|
||||
Some(22299..22333),
|
||||
Cow::Borrowed("Unused blanket `type: ignore` directive"),
|
||||
Severity::Warning,
|
||||
)];
|
||||
|
||||
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
|
||||
SystemPathBuf::from("src").join(file.name())
|
||||
|
||||
@@ -197,7 +197,7 @@ def test_index_with_attribute():
|
||||
ds["Foo"] = ds.Foo + 2.0
|
||||
assert (
|
||||
ds["Foo"] is ds.Foo
|
||||
) # This is now modfied, but both methods points to the same object
|
||||
) # This is now modified, but both methods points to the same object
|
||||
|
||||
|
||||
def test_getitem_time(ds3):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::system::file_time_now;
|
||||
|
||||
/// A number representing the revision of a file.
|
||||
///
|
||||
/// Two revisions that don't compare equal signify that the file has been modified.
|
||||
@@ -16,7 +18,7 @@ impl FileRevision {
|
||||
}
|
||||
|
||||
pub fn now() -> Self {
|
||||
Self::from(filetime::FileTime::now())
|
||||
Self::from(file_time_now())
|
||||
}
|
||||
|
||||
pub const fn zero() -> Self {
|
||||
@@ -53,13 +55,12 @@ impl From<filetime::FileTime> for FileRevision {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use filetime::FileTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn revision_from_file_time() {
|
||||
let file_time = FileTime::now();
|
||||
let file_time = file_time_now();
|
||||
let revision = FileRevision::from(file_time);
|
||||
|
||||
let revision = revision.as_u128();
|
||||
|
||||
@@ -7,6 +7,7 @@ pub use os::testing::UserConfigDirectoryOverrideGuard;
|
||||
#[cfg(feature = "os")]
|
||||
pub use os::OsSystem;
|
||||
|
||||
use filetime::FileTime;
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
@@ -346,3 +347,27 @@ pub enum GlobErrorKind {
|
||||
IOError(io::Error),
|
||||
NonUtf8Path,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn file_time_now() -> FileTime {
|
||||
FileTime::now()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn file_time_now() -> FileTime {
|
||||
// Copied from FileTime::from_system_time()
|
||||
let time = web_time::SystemTime::now();
|
||||
|
||||
time.duration_since(web_time::UNIX_EPOCH)
|
||||
.map(|d| FileTime::from_unix_time(d.as_secs() as i64, d.subsec_nanos()))
|
||||
.unwrap_or_else(|e| {
|
||||
let until_epoch = e.duration();
|
||||
let (sec_offset, nanos) = if until_epoch.subsec_nanos() == 0 {
|
||||
(0, 0)
|
||||
} else {
|
||||
(-1, 1_000_000_000 - until_epoch.subsec_nanos())
|
||||
};
|
||||
|
||||
FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use filetime::FileTime;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::system::{
|
||||
walk_directory, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result,
|
||||
SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf,
|
||||
file_time_now, walk_directory, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata,
|
||||
Result, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf,
|
||||
};
|
||||
|
||||
use super::walk_directory::{
|
||||
@@ -163,7 +163,7 @@ impl MemoryFileSystem {
|
||||
|
||||
let file = get_or_create_file(&mut by_path, &normalized)?;
|
||||
file.content = content.to_string();
|
||||
file.last_modified = now();
|
||||
file.last_modified = file_time_now();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -215,7 +215,7 @@ impl MemoryFileSystem {
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(File {
|
||||
content: content.to_string(),
|
||||
last_modified: now(),
|
||||
last_modified: file_time_now(),
|
||||
});
|
||||
}
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
@@ -310,7 +310,7 @@ impl MemoryFileSystem {
|
||||
let mut by_path = self.inner.by_path.write().unwrap();
|
||||
let normalized = self.normalize_path(path.as_ref());
|
||||
|
||||
get_or_create_file(&mut by_path, &normalized)?.last_modified = now();
|
||||
get_or_create_file(&mut by_path, &normalized)?.last_modified = file_time_now();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -486,7 +486,7 @@ fn create_dir_all(
|
||||
path.push(component);
|
||||
let entry = paths.entry(path.clone()).or_insert_with(|| {
|
||||
Entry::Directory(Directory {
|
||||
last_modified: now(),
|
||||
last_modified: file_time_now(),
|
||||
})
|
||||
});
|
||||
|
||||
@@ -513,7 +513,7 @@ fn get_or_create_file<'a>(
|
||||
let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
|
||||
Entry::File(File {
|
||||
content: String::new(),
|
||||
last_modified: now(),
|
||||
last_modified: file_time_now(),
|
||||
})
|
||||
});
|
||||
|
||||
@@ -695,30 +695,6 @@ enum WalkerState {
|
||||
Nested { path: SystemPathBuf, depth: usize },
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn now() -> FileTime {
|
||||
FileTime::now()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn now() -> FileTime {
|
||||
// Copied from FileTime::from_system_time()
|
||||
let time = web_time::SystemTime::now();
|
||||
|
||||
time.duration_since(web_time::UNIX_EPOCH)
|
||||
.map(|d| FileTime::from_unix_time(d.as_secs() as i64, d.subsec_nanos()))
|
||||
.unwrap_or_else(|e| {
|
||||
let until_epoch = e.duration();
|
||||
let (sec_offset, nanos) = if until_epoch.subsec_nanos() == 0 {
|
||||
(0, 0)
|
||||
} else {
|
||||
(-1, 1_000_000_000 - until_epoch.subsec_nanos())
|
||||
};
|
||||
|
||||
FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::ErrorKind;
|
||||
|
||||
@@ -894,7 +894,7 @@ struct PrinterState<'a> {
|
||||
line_suffixes: LineSuffixes<'a>,
|
||||
verbatim_markers: Vec<TextRange>,
|
||||
group_modes: GroupModes,
|
||||
// Re-used queue to measure if a group fits. Optimisation to avoid re-allocating a new
|
||||
// Reused queue to measure if a group fits. Optimisation to avoid re-allocating a new
|
||||
// vec every time a group gets measured
|
||||
fits_stack: Vec<StackFrame>,
|
||||
fits_queue: Vec<std::slice::Iter<'a, FormatElement>>,
|
||||
|
||||
@@ -85,7 +85,7 @@ def decorator_deprecated_operator_args():
|
||||
bdow_op >> bdow_op2
|
||||
|
||||
|
||||
# deprecated filename_template arugment in FileTaskHandler
|
||||
# deprecated filename_template argument in FileTaskHandler
|
||||
S3TaskHandler(filename_template="/tmp/test")
|
||||
HdfsTaskHandler(filename_template="/tmp/test")
|
||||
ElasticsearchTaskHandler(filename_template="/tmp/test")
|
||||
|
||||
@@ -853,14 +853,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::FutureFeatureNotDefined) {
|
||||
pyflakes::rules::future_feature_not_defined(checker, alias);
|
||||
}
|
||||
if checker.enabled(Rule::LateFutureImport) {
|
||||
if checker.semantic.seen_futures_boundary() {
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
pyflakes::rules::LateFutureImport,
|
||||
stmt.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if &alias.name == "*" {
|
||||
if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
|
||||
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
|
||||
|
||||
@@ -26,6 +26,9 @@ use std::path::Path;
|
||||
|
||||
use itertools::Itertools;
|
||||
use log::debug;
|
||||
use ruff_python_parser::semantic_errors::{
|
||||
SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind,
|
||||
};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, IsolationLevel};
|
||||
@@ -63,6 +66,7 @@ use crate::importer::Importer;
|
||||
use crate::noqa::NoqaMapping;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pyflakes::rules::LateFutureImport;
|
||||
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
|
||||
use crate::settings::{flags, LinterSettings};
|
||||
use crate::{docstrings, noqa, Locator};
|
||||
@@ -223,8 +227,13 @@ pub(crate) struct Checker<'a> {
|
||||
last_stmt_end: TextSize,
|
||||
/// A state describing if a docstring is expected or not.
|
||||
docstring_state: DocstringState,
|
||||
/// The target [`PythonVersion`] for version-dependent checks
|
||||
/// The target [`PythonVersion`] for version-dependent checks.
|
||||
target_version: PythonVersion,
|
||||
/// Helper visitor for detecting semantic syntax errors.
|
||||
#[allow(clippy::struct_field_names)]
|
||||
semantic_checker: SemanticSyntaxChecker,
|
||||
/// Errors collected by the `semantic_checker`.
|
||||
semantic_errors: RefCell<Vec<SemanticSyntaxError>>,
|
||||
}
|
||||
|
||||
impl<'a> Checker<'a> {
|
||||
@@ -272,6 +281,8 @@ impl<'a> Checker<'a> {
|
||||
last_stmt_end: TextSize::default(),
|
||||
docstring_state: DocstringState::default(),
|
||||
target_version,
|
||||
semantic_checker: SemanticSyntaxChecker::new(),
|
||||
semantic_errors: RefCell::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,10 +523,50 @@ impl<'a> Checker<'a> {
|
||||
pub(crate) const fn target_version(&self) -> PythonVersion {
|
||||
self.target_version
|
||||
}
|
||||
|
||||
fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Checker)) {
|
||||
let mut checker = std::mem::take(&mut self.semantic_checker);
|
||||
f(&mut checker, self);
|
||||
self.semantic_checker = checker;
|
||||
}
|
||||
}
|
||||
|
||||
impl SemanticSyntaxContext for Checker<'_> {
|
||||
fn seen_docstring_boundary(&self) -> bool {
|
||||
self.semantic.seen_module_docstring_boundary()
|
||||
}
|
||||
|
||||
fn python_version(&self) -> PythonVersion {
|
||||
self.target_version
|
||||
}
|
||||
|
||||
fn report_semantic_error(&self, error: SemanticSyntaxError) {
|
||||
match error.kind {
|
||||
SemanticSyntaxErrorKind::LateFutureImport => {
|
||||
if self.settings.rules.enabled(Rule::LateFutureImport) {
|
||||
self.report_diagnostic(Diagnostic::new(LateFutureImport, error.range));
|
||||
}
|
||||
}
|
||||
SemanticSyntaxErrorKind::ReboundComprehensionVariable
|
||||
| SemanticSyntaxErrorKind::DuplicateTypeParameter
|
||||
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
|
||||
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_)
|
||||
if self.settings.preview.is_enabled() =>
|
||||
{
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
SemanticSyntaxErrorKind::ReboundComprehensionVariable
|
||||
| SemanticSyntaxErrorKind::DuplicateTypeParameter
|
||||
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
|
||||
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for Checker<'a> {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context));
|
||||
|
||||
// Step 0: Pre-processing
|
||||
self.semantic.push_node(stmt);
|
||||
|
||||
@@ -550,17 +601,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
{
|
||||
self.semantic.flags |= SemanticModelFlags::FUTURE_ANNOTATIONS;
|
||||
}
|
||||
} else {
|
||||
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
|
||||
}
|
||||
}
|
||||
Stmt::Import(_) => {
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
|
||||
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
|
||||
}
|
||||
_ => {
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
|
||||
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
|
||||
if !(self.semantic.seen_import_boundary()
|
||||
|| stmt.is_ipy_escape_command_stmt()
|
||||
|| helpers::is_assignment_to_a_dunder(stmt)
|
||||
@@ -1131,6 +1178,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context));
|
||||
|
||||
// Step 0: Pre-processing
|
||||
if self.source_type.is_stub()
|
||||
&& self.semantic.in_class_base()
|
||||
@@ -2674,7 +2723,7 @@ pub(crate) fn check_ast(
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
notebook_index: Option<&NotebookIndex>,
|
||||
target_version: PythonVersion,
|
||||
) -> Vec<Diagnostic> {
|
||||
) -> (Vec<Diagnostic>, Vec<SemanticSyntaxError>) {
|
||||
let module_path = package
|
||||
.map(PackageRoot::path)
|
||||
.and_then(|package| to_module_path(package, path));
|
||||
@@ -2739,5 +2788,11 @@ pub(crate) fn check_ast(
|
||||
checker.analyze.scopes.push(ScopeId::global());
|
||||
analyze::deferred_scopes(&checker);
|
||||
|
||||
checker.diagnostics.take()
|
||||
let Checker {
|
||||
diagnostics,
|
||||
semantic_errors,
|
||||
..
|
||||
} = checker;
|
||||
|
||||
(diagnostics.into_inner(), semantic_errors.into_inner())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::Path;
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
@@ -43,9 +44,9 @@ pub struct LinterResult {
|
||||
/// Flag indicating that the parsed source code does not contain any
|
||||
/// [`ParseError`]s
|
||||
has_valid_syntax: bool,
|
||||
/// Flag indicating that the parsed source code does not contain any
|
||||
/// [`UnsupportedSyntaxError`]s
|
||||
has_no_unsupported_syntax_errors: bool,
|
||||
/// Flag indicating that the parsed source code does not contain any [`ParseError`]s,
|
||||
/// [`UnsupportedSyntaxError`]s, or [`SemanticSyntaxError`]s.
|
||||
has_no_syntax_errors: bool,
|
||||
}
|
||||
|
||||
impl LinterResult {
|
||||
@@ -62,7 +63,7 @@ impl LinterResult {
|
||||
///
|
||||
/// See [`LinterResult::has_valid_syntax`] for a version specific to [`ParseError`]s.
|
||||
pub fn has_no_syntax_errors(&self) -> bool {
|
||||
self.has_valid_syntax() && self.has_no_unsupported_syntax_errors
|
||||
self.has_valid_syntax() && self.has_no_syntax_errors
|
||||
}
|
||||
|
||||
/// Returns `true` if the parsed source code is valid i.e., it has no [`ParseError`]s.
|
||||
@@ -114,6 +115,9 @@ pub fn check_path(
|
||||
// Aggregate all diagnostics.
|
||||
let mut diagnostics = vec![];
|
||||
|
||||
// Aggregate all semantic syntax errors.
|
||||
let mut semantic_syntax_errors = vec![];
|
||||
|
||||
let tokens = parsed.tokens();
|
||||
let comment_ranges = indexer.comment_ranges();
|
||||
|
||||
@@ -172,35 +176,33 @@ pub fn check_path(
|
||||
|
||||
// Run the AST-based rules only if there are no syntax errors.
|
||||
if parsed.has_valid_syntax() {
|
||||
let use_ast = settings
|
||||
.rules
|
||||
.iter_enabled()
|
||||
.any(|rule_code| rule_code.lint_source().is_ast());
|
||||
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
|
||||
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
|
||||
|
||||
let (new_diagnostics, new_semantic_syntax_errors) = check_ast(
|
||||
parsed,
|
||||
locator,
|
||||
stylist,
|
||||
indexer,
|
||||
&directives.noqa_line_for,
|
||||
settings,
|
||||
noqa,
|
||||
path,
|
||||
package,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
notebook_index,
|
||||
target_version,
|
||||
);
|
||||
diagnostics.extend(new_diagnostics);
|
||||
semantic_syntax_errors.extend(new_semantic_syntax_errors);
|
||||
|
||||
let use_imports = !directives.isort.skip_file
|
||||
&& settings
|
||||
.rules
|
||||
.iter_enabled()
|
||||
.any(|rule_code| rule_code.lint_source().is_imports());
|
||||
if use_ast || use_imports || use_doc_lines {
|
||||
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
|
||||
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
|
||||
if use_ast {
|
||||
diagnostics.extend(check_ast(
|
||||
parsed,
|
||||
locator,
|
||||
stylist,
|
||||
indexer,
|
||||
&directives.noqa_line_for,
|
||||
settings,
|
||||
noqa,
|
||||
path,
|
||||
package,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
notebook_index,
|
||||
target_version,
|
||||
));
|
||||
}
|
||||
if use_imports || use_doc_lines {
|
||||
if use_imports {
|
||||
let import_diagnostics = check_imports(
|
||||
parsed,
|
||||
@@ -368,6 +370,7 @@ pub fn check_path(
|
||||
diagnostics,
|
||||
parsed.errors(),
|
||||
syntax_errors,
|
||||
&semantic_syntax_errors,
|
||||
path,
|
||||
locator,
|
||||
directives,
|
||||
@@ -482,9 +485,9 @@ pub fn lint_only(
|
||||
);
|
||||
|
||||
LinterResult {
|
||||
messages,
|
||||
has_valid_syntax: parsed.has_valid_syntax(),
|
||||
has_no_unsupported_syntax_errors: parsed.unsupported_syntax_errors().is_empty(),
|
||||
has_no_syntax_errors: !messages.iter().any(Message::is_syntax_error),
|
||||
messages,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,6 +496,7 @@ fn diagnostics_to_messages(
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
parse_errors: &[ParseError],
|
||||
unsupported_syntax_errors: &[UnsupportedSyntaxError],
|
||||
semantic_syntax_errors: &[SemanticSyntaxError],
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
directives: &Directives,
|
||||
@@ -514,6 +518,11 @@ fn diagnostics_to_messages(
|
||||
.chain(unsupported_syntax_errors.iter().map(|syntax_error| {
|
||||
Message::from_unsupported_syntax_error(syntax_error, file.deref().clone())
|
||||
}))
|
||||
.chain(
|
||||
semantic_syntax_errors
|
||||
.iter()
|
||||
.map(|error| Message::from_semantic_syntax_error(error, file.deref().clone())),
|
||||
)
|
||||
.chain(diagnostics.into_iter().map(|diagnostic| {
|
||||
let noqa_offset = directives.noqa_line_for.resolve(diagnostic.start());
|
||||
Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_offset)
|
||||
@@ -545,7 +554,7 @@ pub fn lint_fix<'a>(
|
||||
let mut has_valid_syntax = false;
|
||||
|
||||
// Track whether the _initial_ source code has no unsupported syntax errors.
|
||||
let mut has_no_unsupported_syntax_errors = false;
|
||||
let mut has_no_syntax_errors = false;
|
||||
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
|
||||
@@ -589,12 +598,12 @@ pub fn lint_fix<'a>(
|
||||
|
||||
if iterations == 0 {
|
||||
has_valid_syntax = parsed.has_valid_syntax();
|
||||
has_no_unsupported_syntax_errors = parsed.unsupported_syntax_errors().is_empty();
|
||||
has_no_syntax_errors = !messages.iter().any(Message::is_syntax_error);
|
||||
} else {
|
||||
// If the source code had no syntax errors on the first pass, but
|
||||
// does on a subsequent pass, then we've introduced a
|
||||
// syntax error. Return the original code.
|
||||
if has_valid_syntax && has_no_unsupported_syntax_errors {
|
||||
if has_valid_syntax && has_no_syntax_errors {
|
||||
if let Some(error) = parsed.errors().first() {
|
||||
report_fix_syntax_error(
|
||||
path,
|
||||
@@ -636,7 +645,7 @@ pub fn lint_fix<'a>(
|
||||
result: LinterResult {
|
||||
messages,
|
||||
has_valid_syntax,
|
||||
has_no_unsupported_syntax_errors,
|
||||
has_no_syntax_errors,
|
||||
},
|
||||
transformed,
|
||||
fixed,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub use azure::AzureEmitter;
|
||||
@@ -135,6 +136,18 @@ impl Message {
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a [`Message`] from the given [`SemanticSyntaxError`].
|
||||
pub fn from_semantic_syntax_error(
|
||||
semantic_syntax_error: &SemanticSyntaxError,
|
||||
file: SourceFile,
|
||||
) -> Message {
|
||||
Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: format!("SyntaxError: {semantic_syntax_error}"),
|
||||
range: semantic_syntax_error.range,
|
||||
file,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> {
|
||||
match self {
|
||||
Message::Diagnostic(m) => Some(m),
|
||||
|
||||
@@ -241,21 +241,9 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
|
||||
Some("logical_date"),
|
||||
));
|
||||
}
|
||||
["airflow", .., "operators", "datetime", "BranchDateTimeOperator"] => {
|
||||
checker.report_diagnostics(diagnostic_for_argument(
|
||||
arguments,
|
||||
"use_task_execution_day",
|
||||
Some("use_task_logical_date"),
|
||||
));
|
||||
}
|
||||
["airflow", .., "operators", "weekday", "DayOfWeekSensor"] => {
|
||||
checker.report_diagnostics(diagnostic_for_argument(
|
||||
arguments,
|
||||
"use_task_execution_day",
|
||||
Some("use_task_logical_date"),
|
||||
));
|
||||
}
|
||||
["airflow", .., "operators", "weekday", "BranchDayOfWeekOperator"] => {
|
||||
["airflow", .., "operators", "datetime", "BranchDateTimeOperator"]
|
||||
| ["airflow", .., "operators", "weekday", "DayOfWeekSensor" | "BranchDayOfWeekOperator"] =>
|
||||
{
|
||||
checker.report_diagnostics(diagnostic_for_argument(
|
||||
arguments,
|
||||
"use_task_execution_day",
|
||||
@@ -288,29 +276,27 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) {
|
||||
|
||||
let replacement = match *qualname.segments() {
|
||||
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
|
||||
"dataset_factories" => Some(Replacement::Name("asset_factories")),
|
||||
"dataset_uri_handlers" => Some(Replacement::Name("asset_uri_handlers")),
|
||||
"dataset_factories" => Replacement::Name("asset_factories"),
|
||||
"dataset_uri_handlers" => Replacement::Name("asset_uri_handlers"),
|
||||
"dataset_to_openlineage_converters" => {
|
||||
Some(Replacement::Name("asset_to_openlineage_converters"))
|
||||
Replacement::Name("asset_to_openlineage_converters")
|
||||
}
|
||||
_ => None,
|
||||
_ => return,
|
||||
},
|
||||
["airflow", "lineage", "hook", "DatasetLineageInfo"] => match attr.as_str() {
|
||||
"dataset" => Some(Replacement::Name("asset")),
|
||||
_ => None,
|
||||
"dataset" => Replacement::Name("asset"),
|
||||
_ => return,
|
||||
},
|
||||
_ => None,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if let Some(replacement) = replacement {
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
Airflow3Removal {
|
||||
deprecated: attr.to_string(),
|
||||
replacement,
|
||||
},
|
||||
attr.range(),
|
||||
));
|
||||
}
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
Airflow3Removal {
|
||||
deprecated: attr.to_string(),
|
||||
replacement,
|
||||
},
|
||||
attr.range(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Checks whether an Airflow 3.0–removed context key is used in a function decorated with `@task`.
|
||||
@@ -465,68 +451,66 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
|
||||
|
||||
let replacement = match qualname.segments() {
|
||||
["airflow", "datasets", "manager", "DatasetManager"] => match attr.as_str() {
|
||||
"register_dataset_change" => Some(Replacement::Name("register_asset_change")),
|
||||
"create_datasets" => Some(Replacement::Name("create_assets")),
|
||||
"notify_dataset_created" => Some(Replacement::Name("notify_asset_created")),
|
||||
"notify_dataset_changed" => Some(Replacement::Name("notify_asset_changed")),
|
||||
"notify_dataset_alias_created" => Some(Replacement::Name("notify_asset_alias_created")),
|
||||
_ => None,
|
||||
"register_dataset_change" => Replacement::Name("register_asset_change"),
|
||||
"create_datasets" => Replacement::Name("create_assets"),
|
||||
"notify_dataset_created" => Replacement::Name("notify_asset_created"),
|
||||
"notify_dataset_changed" => Replacement::Name("notify_asset_changed"),
|
||||
"notify_dataset_alias_created" => Replacement::Name("notify_asset_alias_created"),
|
||||
_ => return,
|
||||
},
|
||||
["airflow", "lineage", "hook", "HookLineageCollector"] => match attr.as_str() {
|
||||
"create_dataset" => Some(Replacement::Name("create_asset")),
|
||||
"add_input_dataset" => Some(Replacement::Name("add_input_asset")),
|
||||
"add_output_dataset" => Some(Replacement::Name("add_output_asset")),
|
||||
"collected_datasets" => Some(Replacement::Name("collected_assets")),
|
||||
_ => None,
|
||||
"create_dataset" => Replacement::Name("create_asset"),
|
||||
"add_input_dataset" => Replacement::Name("add_input_asset"),
|
||||
"add_output_dataset" => Replacement::Name("add_output_asset"),
|
||||
"collected_datasets" => Replacement::Name("collected_assets"),
|
||||
_ => return,
|
||||
},
|
||||
["airflow", "providers", "amazon", "auth_manager", "aws_auth_manager", "AwsAuthManager"] => {
|
||||
match attr.as_str() {
|
||||
"is_authorized_dataset" => Some(Replacement::Name("is_authorized_asset")),
|
||||
_ => None,
|
||||
"is_authorized_dataset" => Replacement::Name("is_authorized_asset"),
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
|
||||
"initialize_providers_dataset_uri_resources" => Some(Replacement::Name(
|
||||
"initialize_providers_asset_uri_resources",
|
||||
)),
|
||||
_ => None,
|
||||
"initialize_providers_dataset_uri_resources" => {
|
||||
Replacement::Name("initialize_providers_asset_uri_resources")
|
||||
}
|
||||
_ => return,
|
||||
},
|
||||
["airflow", "secrets", "local_filesystem", "LocalFilesystemBackend"] => match attr.as_str()
|
||||
{
|
||||
"get_connections" => Some(Replacement::Name("get_connection")),
|
||||
_ => None,
|
||||
"get_connections" => Replacement::Name("get_connection"),
|
||||
_ => return,
|
||||
},
|
||||
["airflow", "datasets", ..] | ["airflow", "Dataset"] => match attr.as_str() {
|
||||
"iter_datasets" => Some(Replacement::Name("iter_assets")),
|
||||
"iter_dataset_aliases" => Some(Replacement::Name("iter_asset_aliases")),
|
||||
_ => None,
|
||||
"iter_datasets" => Replacement::Name("iter_assets"),
|
||||
"iter_dataset_aliases" => Replacement::Name("iter_asset_aliases"),
|
||||
_ => return,
|
||||
},
|
||||
segments => {
|
||||
if is_airflow_secret_backend(segments) {
|
||||
match attr.as_str() {
|
||||
"get_conn_uri" => Some(Replacement::Name("get_conn_value")),
|
||||
"get_connections" => Some(Replacement::Name("get_connection")),
|
||||
_ => None,
|
||||
"get_conn_uri" => Replacement::Name("get_conn_value"),
|
||||
"get_connections" => Replacement::Name("get_connection"),
|
||||
_ => return,
|
||||
}
|
||||
} else if is_airflow_hook(segments) {
|
||||
match attr.as_str() {
|
||||
"get_connections" => Some(Replacement::Name("get_connection")),
|
||||
_ => None,
|
||||
"get_connections" => Replacement::Name("get_connection"),
|
||||
_ => return,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(replacement) = replacement {
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
Airflow3Removal {
|
||||
deprecated: attr.to_string(),
|
||||
replacement,
|
||||
},
|
||||
attr.range(),
|
||||
));
|
||||
}
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
Airflow3Removal {
|
||||
deprecated: attr.to_string(),
|
||||
replacement,
|
||||
},
|
||||
attr.range(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Check whether a removed Airflow name is used.
|
||||
@@ -613,25 +597,18 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
}
|
||||
|
||||
// airflow.datasets
|
||||
["airflow", "Dataset"] => Replacement::Name("airflow.sdk.definitions.asset.Asset"),
|
||||
["airflow", "Dataset"] | ["airflow", "datasets", "Dataset"] => {
|
||||
Replacement::Name("airflow.sdk.Asset")
|
||||
}
|
||||
["airflow", "datasets", "DatasetAliasEvent"] => Replacement::None,
|
||||
["airflow", "datasets", "Dataset"] => {
|
||||
Replacement::Name("airflow.sdk.definitions.asset.Asset")
|
||||
}
|
||||
["airflow", "datasets", "DatasetAlias"] => {
|
||||
Replacement::Name("airflow.sdk.definitions.asset.AssetAlias")
|
||||
}
|
||||
["airflow", "datasets", "DatasetAll"] => {
|
||||
Replacement::Name("airflow.sdk.definitions.asset.AssetAll")
|
||||
}
|
||||
["airflow", "datasets", "DatasetAny"] => {
|
||||
Replacement::Name("airflow.sdk.definitions.asset.AssetAny")
|
||||
}
|
||||
["airflow", "datasets", "DatasetAlias"] => Replacement::Name("airflow.sdk.AssetAlias"),
|
||||
["airflow", "datasets", "DatasetAll"] => Replacement::Name("airflow.sdk.AssetAll"),
|
||||
["airflow", "datasets", "DatasetAny"] => Replacement::Name("airflow.sdk.AssetAny"),
|
||||
["airflow", "datasets", "expand_alias_to_datasets"] => {
|
||||
Replacement::Name("airflow.sdk.definitions.asset.expand_alias_to_assets")
|
||||
Replacement::Name("airflow.sdk.expand_alias_to_assets")
|
||||
}
|
||||
["airflow", "datasets", "metadata", "Metadata"] => {
|
||||
Replacement::Name("airflow.sdk.definitions.asset.metadata.Metadata")
|
||||
Replacement::Name("airflow.sdk.Metadata")
|
||||
}
|
||||
|
||||
// airflow.datasets.manager
|
||||
|
||||
@@ -228,7 +228,7 @@ AIR302_args.py:67:9: AIR302 `sla` is removed in Airflow 3.0
|
||||
|
||||
AIR302_args.py:89:15: AIR302 `filename_template` is removed in Airflow 3.0
|
||||
|
|
||||
88 | # deprecated filename_template arugment in FileTaskHandler
|
||||
88 | # deprecated filename_template argument in FileTaskHandler
|
||||
89 | S3TaskHandler(filename_template="/tmp/test")
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
90 | HdfsTaskHandler(filename_template="/tmp/test")
|
||||
@@ -237,7 +237,7 @@ AIR302_args.py:89:15: AIR302 `filename_template` is removed in Airflow 3.0
|
||||
|
||||
AIR302_args.py:90:17: AIR302 `filename_template` is removed in Airflow 3.0
|
||||
|
|
||||
88 | # deprecated filename_template arugment in FileTaskHandler
|
||||
88 | # deprecated filename_template argument in FileTaskHandler
|
||||
89 | S3TaskHandler(filename_template="/tmp/test")
|
||||
90 | HdfsTaskHandler(filename_template="/tmp/test")
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
AIR302_class_attribute.py:24:21: AIR302 `airflow.Dataset` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -10,7 +9,7 @@ AIR302_class_attribute.py:24:21: AIR302 `airflow.Dataset` is removed in Airflow
|
||||
25 | dataset_from_root.iter_datasets()
|
||||
26 | dataset_from_root.iter_dataset_aliases()
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.Asset` instead
|
||||
= help: Use `airflow.sdk.Asset` instead
|
||||
|
||||
AIR302_class_attribute.py:25:19: AIR302 `iter_datasets` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -41,7 +40,7 @@ AIR302_class_attribute.py:29:31: AIR302 `airflow.datasets.Dataset` is removed in
|
||||
30 | dataset_to_test_method_call.iter_datasets()
|
||||
31 | dataset_to_test_method_call.iter_dataset_aliases()
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.Asset` instead
|
||||
= help: Use `airflow.sdk.Asset` instead
|
||||
|
||||
AIR302_class_attribute.py:30:29: AIR302 `iter_datasets` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -73,7 +72,7 @@ AIR302_class_attribute.py:33:29: AIR302 `airflow.datasets.DatasetAlias` is remov
|
||||
34 | alias_to_test_method_call.iter_datasets()
|
||||
35 | alias_to_test_method_call.iter_dataset_aliases()
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.AssetAlias` instead
|
||||
= help: Use `airflow.sdk.AssetAlias` instead
|
||||
|
||||
AIR302_class_attribute.py:34:27: AIR302 `iter_datasets` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -104,7 +103,7 @@ AIR302_class_attribute.py:37:27: AIR302 `airflow.datasets.DatasetAny` is removed
|
||||
38 | any_to_test_method_call.iter_datasets()
|
||||
39 | any_to_test_method_call.iter_dataset_aliases()
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.AssetAny` instead
|
||||
= help: Use `airflow.sdk.AssetAny` instead
|
||||
|
||||
AIR302_class_attribute.py:38:25: AIR302 `iter_datasets` is removed in Airflow 3.0
|
||||
|
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
assertion_line: 29
|
||||
---
|
||||
AIR302_names.py:107:1: AIR302 `airflow.PY36` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -74,7 +73,7 @@ AIR302_names.py:108:1: AIR302 `airflow.Dataset` is removed in Airflow 3.0
|
||||
109 |
|
||||
110 | # airflow.api_connexion.security
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.Asset` instead
|
||||
= help: Use `airflow.sdk.Asset` instead
|
||||
|
||||
AIR302_names.py:111:1: AIR302 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -197,7 +196,7 @@ AIR302_names.py:125:1: AIR302 `airflow.datasets.Dataset` is removed in Airflow 3
|
||||
126 | DatasetAlias()
|
||||
127 | DatasetAliasEvent()
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.Asset` instead
|
||||
= help: Use `airflow.sdk.Asset` instead
|
||||
|
||||
AIR302_names.py:126:1: AIR302 `airflow.datasets.DatasetAlias` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -208,7 +207,7 @@ AIR302_names.py:126:1: AIR302 `airflow.datasets.DatasetAlias` is removed in Airf
|
||||
127 | DatasetAliasEvent()
|
||||
128 | DatasetAll()
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.AssetAlias` instead
|
||||
= help: Use `airflow.sdk.AssetAlias` instead
|
||||
|
||||
AIR302_names.py:127:1: AIR302 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -229,7 +228,7 @@ AIR302_names.py:128:1: AIR302 `airflow.datasets.DatasetAll` is removed in Airflo
|
||||
129 | DatasetAny()
|
||||
130 | Metadata()
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.AssetAll` instead
|
||||
= help: Use `airflow.sdk.AssetAll` instead
|
||||
|
||||
AIR302_names.py:129:1: AIR302 `airflow.datasets.DatasetAny` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -240,7 +239,7 @@ AIR302_names.py:129:1: AIR302 `airflow.datasets.DatasetAny` is removed in Airflo
|
||||
130 | Metadata()
|
||||
131 | expand_alias_to_datasets
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.AssetAny` instead
|
||||
= help: Use `airflow.sdk.AssetAny` instead
|
||||
|
||||
AIR302_names.py:130:1: AIR302 `airflow.datasets.metadata.Metadata` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -250,7 +249,7 @@ AIR302_names.py:130:1: AIR302 `airflow.datasets.metadata.Metadata` is removed in
|
||||
| ^^^^^^^^ AIR302
|
||||
131 | expand_alias_to_datasets
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.metadata.Metadata` instead
|
||||
= help: Use `airflow.sdk.Metadata` instead
|
||||
|
||||
AIR302_names.py:131:1: AIR302 `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0
|
||||
|
|
||||
@@ -261,7 +260,7 @@ AIR302_names.py:131:1: AIR302 `airflow.datasets.expand_alias_to_datasets` is rem
|
||||
132 |
|
||||
133 | # airflow.datasets.manager
|
||||
|
|
||||
= help: Use `airflow.sdk.definitions.asset.expand_alias_to_assets` instead
|
||||
= help: Use `airflow.sdk.expand_alias_to_assets` instead
|
||||
|
||||
AIR302_names.py:135:1: AIR302 `airflow.datasets.manager.dataset_manager` is removed in Airflow 3.0
|
||||
|
|
||||
|
||||
@@ -3124,7 +3124,7 @@ lambda: fu
|
||||
|
||||
#[test]
|
||||
fn redefined_by_gen_exp() {
|
||||
// Re-using a global name as the loop variable for a generator
|
||||
// Reusing a global name as the loop variable for a generator
|
||||
// expression results in a redefinition warning.
|
||||
flakes(
|
||||
"import fu; (1 for fu in range(1))",
|
||||
|
||||
@@ -31,6 +31,7 @@ use ruff_text_size::Ranged;
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// fruits = ["orange", "apple"]
|
||||
/// vegetables = []
|
||||
///
|
||||
/// if fruits:
|
||||
/// print(fruits)
|
||||
|
||||
@@ -9,21 +9,37 @@ use crate::{checkers::ast::Checker, importer::ImportRequest};
|
||||
/// Checks for subclasses of `dict`, `list` or `str`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Subclassing `dict`, `list`, or `str` objects can be error prone, use the
|
||||
/// `UserDict`, `UserList`, and `UserString` objects from the `collections` module
|
||||
/// Built-in types don't consistently use their own dunder methods. For example,
|
||||
/// `dict.__init__` and `dict.update()` bypass `__setitem__`, making inheritance unreliable.
|
||||
///
|
||||
/// Use the `UserDict`, `UserList`, and `UserString` objects from the `collections` module
|
||||
/// instead.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class CaseInsensitiveDict(dict): ...
|
||||
/// class UppercaseDict(dict):
|
||||
/// def __setitem__(self, key, value):
|
||||
/// super().__setitem__(key.upper(), value)
|
||||
///
|
||||
///
|
||||
/// d = UppercaseDict({"a": 1, "b": 2}) # Bypasses __setitem__
|
||||
/// print(d) # {'a': 1, 'b': 2}
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from collections import UserDict
|
||||
///
|
||||
///
|
||||
/// class CaseInsensitiveDict(UserDict): ...
|
||||
/// class UppercaseDict(UserDict):
|
||||
/// def __setitem__(self, key, value):
|
||||
/// super().__setitem__(key.upper(), value)
|
||||
///
|
||||
///
|
||||
/// d = UppercaseDict({"a": 1, "b": 2}) # Uses __setitem__
|
||||
/// print(d) # {'A': 1, 'B': 2}
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
|
||||
@@ -104,7 +104,7 @@ fields = [{ name = "targets", type = "Expr*" }]
|
||||
doc = "See also [TypeAlias](https://docs.python.org/3/library/ast.html#ast.TypeAlias)"
|
||||
fields = [
|
||||
{ name = "name", type = "Expr" },
|
||||
{ name = "type_params", type = "TypeParams?" },
|
||||
{ name = "type_params", type = "Box<crate::TypeParams>?" },
|
||||
{ name = "value", type = "Expr" },
|
||||
]
|
||||
|
||||
|
||||
2
crates/ruff_python_ast/src/generated.rs
generated
2
crates/ruff_python_ast/src/generated.rs
generated
@@ -6494,7 +6494,7 @@ pub struct StmtDelete {
|
||||
pub struct StmtTypeAlias {
|
||||
pub range: ruff_text_size::TextRange,
|
||||
pub name: Box<Expr>,
|
||||
pub type_params: Option<crate::TypeParams>,
|
||||
pub type_params: Option<Box<crate::TypeParams>>,
|
||||
pub value: Box<Expr>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1622,10 +1622,10 @@ mod tests {
|
||||
});
|
||||
let type_alias = Stmt::TypeAlias(StmtTypeAlias {
|
||||
name: Box::new(name.clone()),
|
||||
type_params: Some(TypeParams {
|
||||
type_params: Some(Box::new(TypeParams {
|
||||
type_params: vec![type_var_one, type_var_two],
|
||||
range: TextRange::default(),
|
||||
}),
|
||||
})),
|
||||
value: Box::new(constant_three.clone()),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
@@ -2244,12 +2244,33 @@ impl Pattern {
|
||||
///
|
||||
/// [irrefutable pattern]: https://peps.python.org/pep-0634/#irrefutable-case-blocks
|
||||
pub fn is_irrefutable(&self) -> bool {
|
||||
self.irrefutable_pattern().is_some()
|
||||
}
|
||||
|
||||
/// Return `Some(IrrefutablePattern)` if `self` is irrefutable or `None` otherwise.
|
||||
pub fn irrefutable_pattern(&self) -> Option<IrrefutablePattern> {
|
||||
match self {
|
||||
Pattern::MatchAs(PatternMatchAs { pattern: None, .. }) => true,
|
||||
Pattern::MatchAs(PatternMatchAs {
|
||||
pattern,
|
||||
name,
|
||||
range,
|
||||
}) => match pattern {
|
||||
Some(pattern) => pattern.irrefutable_pattern(),
|
||||
None => match name {
|
||||
Some(name) => Some(IrrefutablePattern {
|
||||
kind: IrrefutablePatternKind::Name(name.id.clone()),
|
||||
range: *range,
|
||||
}),
|
||||
None => Some(IrrefutablePattern {
|
||||
kind: IrrefutablePatternKind::Wildcard,
|
||||
range: *range,
|
||||
}),
|
||||
},
|
||||
},
|
||||
Pattern::MatchOr(PatternMatchOr { patterns, .. }) => {
|
||||
patterns.iter().any(Pattern::is_irrefutable)
|
||||
patterns.iter().find_map(Pattern::irrefutable_pattern)
|
||||
}
|
||||
_ => false,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2277,6 +2298,17 @@ impl Pattern {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IrrefutablePattern {
|
||||
pub kind: IrrefutablePatternKind,
|
||||
pub range: TextRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum IrrefutablePatternKind {
|
||||
Name(Name),
|
||||
Wildcard,
|
||||
}
|
||||
|
||||
/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PatternMatchValue {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
type Alias[T, T] = ...
|
||||
def f[T, T](t: T): ...
|
||||
class C[T, T]: ...
|
||||
type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ...
|
||||
def f[T, T, T](): ... # two errors
|
||||
def f[T, *T](): ... # star is still duplicate
|
||||
def f[T, **T](): ... # as is double star
|
||||
@@ -0,0 +1,12 @@
|
||||
match x:
|
||||
case var: ... # capture pattern
|
||||
case 2: ...
|
||||
match x:
|
||||
case _: ...
|
||||
case 2: ... # wildcard pattern
|
||||
match x:
|
||||
case var1 as var2: ... # as pattern with irrefutable left-hand side
|
||||
case 2: ...
|
||||
match x:
|
||||
case enum.variant | var: ... # or pattern with irrefutable part
|
||||
case 2: ...
|
||||
@@ -0,0 +1,10 @@
|
||||
match 2:
|
||||
case [y, z, y]: ... # MatchSequence
|
||||
case [y, z, *y]: ... # MatchSequence
|
||||
case [y, y, y]: ... # MatchSequence multiple
|
||||
case {1: x, 2: x}: ... # MatchMapping duplicate pattern
|
||||
case {1: x, **x}: ... # MatchMapping duplicate in **rest
|
||||
case Class(x, x): ... # MatchClass positional
|
||||
case Class(x=1, x=2): ... # MatchClass keyword
|
||||
case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr
|
||||
case x as x: ... # MatchAs
|
||||
@@ -0,0 +1,9 @@
|
||||
[(a := 0) for a in range(0)]
|
||||
{(a := 0) for a in range(0)}
|
||||
{(a := 0): val for a in range(0)}
|
||||
{key: (a := 0) for a in range(0)}
|
||||
((a := 0) for a in range(0))
|
||||
[[(a := 0)] for a in range(0)]
|
||||
[(a := 0) for b in range (0) for a in range(0)]
|
||||
[(a := 0) for a in range (0) for b in range(0)]
|
||||
[((a := 0), (b := 1)) for a in range (0) for b in range(0)]
|
||||
@@ -0,0 +1,9 @@
|
||||
match x:
|
||||
case 2: ...
|
||||
case var: ...
|
||||
match x:
|
||||
case 2: ...
|
||||
case _: ...
|
||||
match x:
|
||||
case var if True: ... # don't try to refute a guarded pattern
|
||||
case 2: ...
|
||||
@@ -1,3 +1,4 @@
|
||||
match foo:
|
||||
case foo_bar: ...
|
||||
match foo:
|
||||
case _: ...
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
match foo:
|
||||
case case: ...
|
||||
match foo:
|
||||
case match: ...
|
||||
match foo:
|
||||
case type: ...
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
match subject:
|
||||
case a: ...
|
||||
case a if x: ...
|
||||
case a, b: ...
|
||||
case a, b if x: ...
|
||||
case a: ...
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
match 2:
|
||||
case Class(x) | [x] | x: ...
|
||||
@@ -0,0 +1,5 @@
|
||||
type Alias[T] = list[T]
|
||||
def f[T](t: T): ...
|
||||
class C[T]: ...
|
||||
class C[T, U, V]: ...
|
||||
type Alias[T, U: str, V: (str, bytes), *Ts, **P, D = default] = ...
|
||||
@@ -0,0 +1 @@
|
||||
[a := 0 for x in range(0)]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user